Chip group
Chip group — a short, closed set of filters, drawn as pills that wrap.
Examples
<fieldset class="chip-group">
<legend class="field-label">When</legend>
<div class="chip-list">
<input class="chip-input" type="radio" name="demo-when" id="demo-when-any" value="" checked />
<label class="chip" for="demo-when-any">Any time</label>
<input class="chip-input" type="radio" name="demo-when" id="demo-when-today" value="today" />
<label class="chip" for="demo-when-today">Today</label>
<input class="chip-input" type="radio" name="demo-when" id="demo-when-weekend" value="weekend" />
<label class="chip" for="demo-when-weekend">This weekend</label>
<input class="chip-input" type="radio" name="demo-when" id="demo-when-month" value="month" />
<label class="chip" for="demo-when-month">This month</label>
</div>
</fieldset>A filter, not a control: the options are content, the row wraps, and the first chip is a real "no filter" option with an empty value rather than the absence of a selection — which is what makes clearing the filter something a reader can click.
No JavaScript: :checked draws the selection and the value is read off the form. Reach for segmented instead when the choice is between two or three views of one thing and the strip must never wrap.
<fieldset class="chip-group chip-group-sm">
<legend class="field-label">Filters</legend>
<div class="chip-list">
<input class="chip-input" type="checkbox" name="demo-tags" id="demo-tag-free" value="free" checked />
<label class="chip" for="demo-tag-free">Free entry</label>
<input class="chip-input" type="checkbox" name="demo-tags" id="demo-tag-indoor" value="indoor" />
<label class="chip" for="demo-tag-indoor">Indoors</label>
<input class="chip-input" type="checkbox" name="demo-tags" id="demo-tag-kids" value="kids" checked />
<label class="chip" for="demo-tag-kids">For children</label>
<input class="chip-input" type="checkbox" name="demo-tags" id="demo-tag-soon" value="soon" disabled />
<label class="chip" for="demo-tag-soon">Sold out</label>
</div>
</fieldset>Swap the input type and nothing else changes: the pill does not care what is under it, and the platform announces "checkbox" or "radio button" correctly either way.
A disabled chip that is checked still reads as checked — the filter is on, it just cannot be turned off from here.
Tokens 12
Level 2, declared on .chip-group itself. Set any of them on that selector to restyle this component without touching the skin.
| Token | Default |
|---|---|
| --chip-gap | var(--space-2) |
| --chip-min-size | var(--target-size-compact) |
| --chip-bg | var(--color-surface) |
| --chip-border-color | var(--color-border) |
| --chip-color | var(--color-on-surface) |
| --chip-font-size | var(--font-size-sm) |
| --chip-hover-bg | var(--color-surface-sunken) |
| --chip-padding-inline | var(--space-4) |
| --chip-radius | var(--radius-full) |
| --chip-selected-bg | var(--color-primary) |
| --chip-selected-border-color | var(--color-primary) |
| --chip-selected-color | var(--color-on-primary) |
Variants and states
Variants
.chip-group-lg.chip-group-sm
Inside it
.chip.chip-input.chip-list.field-label
State it reads
Read from the platform, never mirrored into a class that could disagree with it.
:checked:disabled:focus:hover
Before you ship it
What you have to do 6 requirements
- Name the group. A
<legend>inside<fieldset>, orrole="group"witharia-labelon any other container. Without it the chips are announced one by one with no idea what they filter. - Give one radio
checked. An empty radio group makes the first Tab land on the first chip rather than on the chosen one, and there is no chosen one. Checkboxes have no such rule: none checked is a real state. - Radios get an "All" chip; checkboxes must not have one. See above. A checkbox group needs removable tags and a "Clear filters" control instead, or the reader has no way out of the filter.
- If the chips carry counts, write the label's shape once. One function, called by the chips and by every other control that shows the same set, and the number goes inside the label — a badge beside the chip is a number announced with nothing attached to it, and a
<select>showing the same options cannot hold one at all. - Keep the set short and closed. These are all visible at once, which is the whole reason to use them instead of a
select. Past about ten, or when the labels are long, aselectis the honest control. - The label must follow its input. The selected and focused states are
+rules; markup that puts the input inside the label, or anywhere else, renders a chip that never lights up.
src/css/components/chip-group.css · npx mostlycss add chip-group