Stripe chips

Each stripe is one requirement of the club's grading system (default for TKD Bern: Form, Kyorugi, 5 Tenets, Hosinsul, Theorie). Look (design guide §9.7): --radius-sm chip with a small colour square (outlined with --belt-edge) and the name. Earned: --color-surface with a --color-border-strong border, check icon, --color-text. Not earned: dashed --color-border outline, --color-text-muted, no check. The state is also in the accessible name ("Form: erhalten").

Read-only by default. With editable: true every stripe is a toggle button; the page listens for click on [data-stripe] and saves the change (with the offline outbox in the Trainer app).

Examples

Read-only (profile, applicant card)

  • Form: erhalten
  • Kyorugi: erhalten
  • 5 Tenets: offen
  • Hosinsul: offen
  • Theorie: offen
Data
render({
  "label": "Streifen für 5. Kup",
  "stripes": [
    {
      "key": "form",
      "label": "Form",
      "color": "red",
      "earned": true,
      "earnedAt": "2026-09-14"
    },
    {
      "key": "kyorugi",
      "label": "Kyorugi",
      "color": "blue",
      "earned": true
    },
    {
      "key": "tenets",
      "label": "5 Tenets",
      "color": "green",
      "earned": false
    },
    {
      "key": "hosinsul",
      "label": "Hosinsul",
      "color": "yellow",
      "earned": false
    },
    {
      "key": "theory",
      "label": "Theorie",
      "color": "black",
      "earned": false
    }
  ]
})
Markup
<ul class="stripes" role="list" aria-label="Streifen für 5. Kup"><li class="stripe" data-stripe="form" style="--stripe-color: var(--stripe-red)" data-earned title="erhalten am 14.09.2026">Form<span class="u-visually-hidden">: erhalten</span><span class="stripe__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span></li><li class="stripe" data-stripe="kyorugi" style="--stripe-color: var(--stripe-blue)" data-earned>Kyorugi<span class="u-visually-hidden">: erhalten</span><span class="stripe__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span></li><li class="stripe" data-stripe="tenets" style="--stripe-color: var(--stripe-green)">5 Tenets<span class="u-visually-hidden">: offen</span><span class="stripe__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span></li><li class="stripe" data-stripe="hosinsul" style="--stripe-color: var(--stripe-yellow)">Hosinsul<span class="u-visually-hidden">: offen</span><span class="stripe__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span></li><li class="stripe" data-stripe="theory" style="--stripe-color: var(--stripe-black)">Theorie<span class="u-visually-hidden">: offen</span><span class="stripe__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span></li></ul>

Editable (Trainer app, student panel)

Data
render({
  "label": "Streifen von Lea Müller",
  "editable": true,
  "stripes": [
    {
      "key": "form",
      "label": "Form",
      "color": "red",
      "earned": true
    },
    {
      "key": "kyorugi",
      "label": "Kyorugi",
      "color": "blue",
      "earned": false
    },
    {
      "key": "tenets",
      "label": "5 Tenets",
      "color": "green",
      "earned": false
    }
  ]
})
Markup
<ul class="stripes" role="list" aria-label="Streifen von Lea Müller"><li><button type="button" class="stripe" data-stripe="form" aria-pressed="true" style="--stripe-color: var(--stripe-red)" data-earned>Form<span class="stripe__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span></button></li><li><button type="button" class="stripe" data-stripe="kyorugi" aria-pressed="false" style="--stripe-color: var(--stripe-blue)">Kyorugi<span class="stripe__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span></button></li><li><button type="button" class="stripe" data-stripe="tenets" aria-pressed="false" style="--stripe-color: var(--stripe-green)">5 Tenets<span class="stripe__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span></button></li></ul>

Data contract

What render(data) accepts — validated against this JSON Schema in development and tests. Unknown fields are rejected. The same schema is in components.json.

FieldTypeRequiredDefaultDescription
labelstringoptional"Streifen"Accessible name of the group, e.g. "Streifen für 5. Kup". Required when several groups are on one page.
editablebooleanoptionalfalseRender toggle buttons (aria-pressed) instead of a static list.
stripesarray of objectsrequired—Requirements in the order of the grading system.
min 1 items · max 10 items
stripes[].keystringrequired—Stable id of the requirement in the grading system, e.g. "form". Written to data-stripe.
pattern ^[a-z0-9-]+$
stripes[].labelstringrequired—Localised requirement name shown on the chip.
min 1 chars
stripes[].color"red" | "blue" | "green" | "yellow" | "black"required—Stripe colour; maps to --stripe-{color}.
stripes[].earnedbooleanrequired—Whether the student has this stripe.
stripes[].earnedAtstringoptional—Date earned (ISO); shown as tooltip "erhalten am …" when set.
pattern ^\d{4}-\d{2}-\d{2}$
JSON Schema
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "stripes"
  ],
  "properties": {
    "label": {
      "type": "string",
      "description": "Accessible name of the group, e.g. \"Streifen für 5. Kup\". Required when several groups are on one page.",
      "default": "Streifen"
    },
    "editable": {
      "type": "boolean",
      "default": false,
      "description": "Render toggle buttons (aria-pressed) instead of a static list."
    },
    "stripes": {
      "type": "array",
      "minItems": 1,
      "maxItems": 10,
      "description": "Requirements in the order of the grading system.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "key",
          "label",
          "color",
          "earned"
        ],
        "properties": {
          "key": {
            "type": "string",
            "pattern": "^[a-z0-9-]+$",
            "description": "Stable id of the requirement in the grading system, e.g. \"form\". Written to `data-stripe`."
          },
          "label": {
            "type": "string",
            "minLength": 1,
            "description": "Localised requirement name shown on the chip."
          },
          "color": {
            "type": "string",
            "enum": [
              "red",
              "blue",
              "green",
              "yellow",
              "black"
            ],
            "description": "Stripe colour; maps to `--stripe-{color}`."
          },
          "earned": {
            "type": "boolean",
            "description": "Whether the student has this stripe."
          },
          "earnedAt": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Date earned (ISO); shown as tooltip \"erhalten am …\" when set."
          }
        }
      }
    }
  }
}

Markup & states

Root: ul.stripes — usable without render() by writing the markup directly.

Attributes

AttributeValuesDefaultDescription
aria-labeltext—Group name (from label).
role="list"——Keeps list semantics with list-style removed.

Parts

PartDescription
.stripeOne requirement (li, or button when editable) with data-stripe="{key}".
.stripe::beforeColour square (--stripe-color, --belt-edge outline), drawn by CSS.
.stripe__check16 px check icon, always rendered, shown only when earned/pressed — so page code only toggles the attributes.
.u-visually-hiddenRead-only chips: ": erhalten" / ": offen" so the state is in the accessible name.

States

StateDescription
[data-earned]Earned: surface background, solid --color-border-strong border, check icon, full text colour.
(not earned)Dashed --color-border outline, muted text, no check.
[aria-pressed="true|false"]Editable form: toggle state, mirrors data-earned (pressed is styled as earned).

CSS custom properties

PropertyDefaultDescription
--stripe-color—Set per chip to var(--stripe-{color}).

Behaviour & events

Module: (page code)

No built-in behaviour. Listen for clicks on [data-stripe] inside the group, toggle aria-pressed/data-earned, save, and show the save status.

Events

EventDetailDescription
clickevent.target.closest("[data-stripe]").dataset.stripe → keyNative click on an editable chip.

Accessibility

  • Every chip shows its name — never colour-only dots (audit finding: stripes were unlabeled dots).
  • Editable chips are buttons with aria-pressed, so screen readers announce "Form, Umschalter, gedrückt".
  • The earned date is a tooltip only; important dates must also be visible elsewhere.

Guidance

DoKeep the order of the grading system.
DoShow the save status next to editable stripes (they are written to the CRM).
Don'tDon't invent a second stripe display; class and belt-test screens share this one.

Related: Belt badge · Save status