Chip group

Filters show as chips above the list they filter ("Offen 3 · Freigegeben 28 · Abgelehnt 2"). Each chip is a <button> with aria-pressed; the row scrolls horizontally on small screens instead of wrapping.

mode: "single" (default) means exactly one chip is pressed at a time — like a segmented status filter; render rejects data with more than one pressed chip. mode: "multi" lets any number be pressed (tags, several groups). The mode is written to data-mode for the page code, which enforces it on click.

There is no built-in behaviour: the page listens for clicks on [data-value] inside the group, updates aria-pressed, writes the filter into the URL query string (UX guide §3.3) and reloads the results. Offer "Filter zurücksetzen" nearby when filters are active.

Counts are the number of results that chip would show, as plain digits. They are part of the chip's accessible name ("Offen 3").

Examples

Single-select status filter

Data
render({
  "label": "Anmeldungen filtern",
  "name": "status",
  "chips": [
    {
      "value": "open",
      "label": "Offen",
      "count": 3,
      "pressed": true
    },
    {
      "value": "approved",
      "label": "Freigegeben",
      "count": 28
    },
    {
      "value": "rejected",
      "label": "Abgelehnt",
      "count": 2
    }
  ]
})
Markup
<div class="chip-row" role="group" aria-label="Anmeldungen filtern" data-mode="single" data-filter="status"><button class="chip" type="button" data-value="open" aria-pressed="true"><span class="chip__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span>Offen <span class="chip__count">3</span></button><button class="chip" type="button" data-value="approved" aria-pressed="false"><span class="chip__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span>Freigegeben <span class="chip__count">28</span></button><button class="chip" type="button" data-value="rejected" aria-pressed="false"><span class="chip__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span>Abgelehnt <span class="chip__count">2</span></button></div>

Multi-select groups

Data
render({
  "label": "Trainingsgruppen",
  "name": "group",
  "mode": "multi",
  "chips": [
    {
      "value": "tigers",
      "label": "Tigers (4–6)",
      "pressed": true
    },
    {
      "value": "kids",
      "label": "Kinder 7–10",
      "pressed": true
    },
    {
      "value": "youth",
      "label": "Jugend 11–15"
    },
    {
      "value": "adults",
      "label": "Erwachsene"
    }
  ]
})
Markup
<div class="chip-row" role="group" aria-label="Trainingsgruppen" data-mode="multi" data-filter="group"><button class="chip" type="button" data-value="tigers" aria-pressed="true"><span class="chip__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span>Tigers (4–6)</button><button class="chip" type="button" data-value="kids" aria-pressed="true"><span class="chip__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span>Kinder 7–10</button><button class="chip" type="button" data-value="youth" aria-pressed="false"><span class="chip__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span>Jugend 11–15</button><button class="chip" type="button" data-value="adults" aria-pressed="false"><span class="chip__check"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></span>Erwachsene</button></div>

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
labelstringrequired—Accessible name of the group (aria-label), e.g. "Status filtern".
min 1 chars
namestringoptional—Filter key (query-string parameter) written to data-filter, e.g. status.
pattern ^[a-z][a-z0-9-]*$
mode"single" | "multi"optional"single"single: exactly one chip pressed (status filter). multi: any number pressed (tags). Written to data-mode.
chipsarray of objectsrequired—Chips in display order. Put "Alle" first for single-select filters.
min 1 items
chips[].valuestringrequired—Filter value written to data-value ("open", "approved").
pattern ^[A-Za-z0-9_-]+$
chips[].labelstringrequired—Chip text ("Offen").
min 1 chars
chips[].countintegeroptional—Number of results for this chip, shown after the label.
≥ 0
chips[].pressedbooleanoptionalfalseChip is active (aria-pressed="true").
JSON Schema
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "label",
    "chips"
  ],
  "properties": {
    "label": {
      "type": "string",
      "minLength": 1,
      "description": "Accessible name of the group (`aria-label`), e.g. \"Status filtern\"."
    },
    "name": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]*$",
      "description": "Filter key (query-string parameter) written to `data-filter`, e.g. `status`."
    },
    "mode": {
      "type": "string",
      "enum": [
        "single",
        "multi"
      ],
      "default": "single",
      "description": "`single`: exactly one chip pressed (status filter). `multi`: any number pressed (tags). Written to `data-mode`."
    },
    "chips": {
      "type": "array",
      "minItems": 1,
      "description": "Chips in display order. Put \"Alle\" first for single-select filters.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "value",
          "label"
        ],
        "properties": {
          "value": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]+$",
            "description": "Filter value written to `data-value` (\"open\", \"approved\")."
          },
          "label": {
            "type": "string",
            "minLength": 1,
            "description": "Chip text (\"Offen\")."
          },
          "count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of results for this chip, shown after the label."
          },
          "pressed": {
            "type": "boolean",
            "default": false,
            "description": "Chip is active (`aria-pressed=\"true\"`)."
          }
        }
      }
    }
  }
}

Markup & states

Root: div.chip-row — usable without render() by writing the markup directly.

Attributes

AttributeValuesDefaultDescription
role="group" aria-labeltext—Names the filter group.
data-filterfilter key—Query-string parameter the group controls (page code).
data-modesingle | multi—Selection mode, enforced by page code.

Parts

PartDescription
.chip<button type="button" aria-pressed data-value> — one filter value. Pill on --color-surface-2, 32 px visual height with a 44 px hit area (::after).
.chip__check16 px check icon, always rendered but shown only while the chip is pressed — so page code only toggles aria-pressed.
.chip__countResult count in muted tabular figures.

States

StateDescription
.chip[aria-pressed="true"]Selected: --color-accent-subtle background, --color-accent-subtle-text and the check icon (design guide §9.3).
.chip:hover / :focus-visibleSurface shift (pointer devices) / global focus ring.

Behaviour & events

Module: (page code)

No built-in behaviour. One delegated listener on the group: group.addEventListener("click", e => { const chip = e.target.closest("[data-value]"); … }). In single mode set aria-pressed="true" on the clicked chip and false on all others; in multi mode toggle it. Then update the URL query (?{data-filter}={value}) and reload results.

Events

EventDetailDescription
clickevent.target.closest("[data-value]").dataset.value → filter value; group.dataset.filter → keyNative click on a chip.

Accessibility

  • Chips are native buttons with aria-pressed, announced as toggle buttons ("Offen 3, Umschalter, gedrückt").
  • The group has role="group" and a label, so the filter purpose is announced when entering it.
  • Selected state is shown by colour and a check icon; the pressed state is also exposed to assistive tech. The icon is aria-hidden.
  • After filtering, announce the new result count (e.g. in a live region "18 von 312 Mitgliedern").

Guidance

DoKeep filters in the URL so reload and back restore them.
DoShow counts so empty filters are visible before clicking.
DoKeep labels to one or two words.
Don'tDon't use chips as status labels — that's a badge.
Don'tDon't hide filters behind a chip row longer than ~8 chips; use a filter sheet instead.
Don'tDon't use chips for navigation between pages — use tabs or nav items.

Related: Status badge · List · Table · Tabs