Chip
Filters a list, or holds a value someone can remove.
Examples
Conversations2 of 6
| cnv_8f2b | refund-agent | Waiting on you |
| cnv_8f26 | billing-agent | Waiting on you |
Styles
secondarydefault
ghost
outline
Group
wrapdefault
strip
Sizes
defaultdefault
xs
States
rest
hover
focus
active
on
disabled
invalid
menu open
Usage
When to use it
- Use
- The person narrows a list by one or more values and sees the list change at once.
- Use
- A field holds values the person chose (filters, recipients, labels) and each can be removed.
- Use
- A filter row needs a menu for one dimension (model, agent) beside on/off chips.
- Not for
- The token only labels something and is never pressed. Use Badge.
- Not for
- The person picks exactly one of two to five views. Use Tabs.
- Not for
- The action starts work: send, refund, save. Use Button.
- Not for
- A setting turns on or off outside the list. Use Switch.
Rules
10 rules
- Must
- Wrap two or more filter chips in
<ChipGroup role="group" aria-label>. Because a screen reader hears "Waiting, toggle button" with nothing to say what the chips filter. - Should
- Pass count as the rows the filter matches now; disable the chip at 0 instead of hiding it. Because a chip that disappears shifts the row under the pointer and hides that the filter exists.
- Must
- Render removable values in one ChipGroup with stable keys so focus moves to the next remove button. Because with index keys React unmounts the last chip instead of the removed one, so the focus target is gone and focus falls to the page.
- Should
- Label a chip with the value itself in 1 to 3 words, not a sentence or a verb.
- Must
- Pass removeLabel when a value holds an element instead of a plain string. Because the default name is built only from string children, so every such remove button is just "Remove".
- Must
- Show on with pressed and the selected wash, kind or no kind; never a hue. Because a class sets no aria-pressed, so a screen reader cannot tell on from off, and hue means a kind in Quartz.
- Never
- Round a chip into a pill; keep the 6px control corner. Because round belongs to person avatars and radios in Quartz, and a pill chip no longer matches the buttons beside it.
- Never
- Add an x to a pressed filter; pressing again turns it off. Because onRemove turns the chip into a value span, so pressed is dropped and the filter loses its on state.
- Should
- Let a value longer than 32 characters shrink; the chip truncates and keeps the remove button.
- Should
- Put toggle filters in a table toolbar in a ChipGroup variant="strip", at size="xs". Because the toolbar is 40px, and a 24px chip sits 8px from its rules; a bare div loses the group name and the one-line row.
Accessibility
7 checks
- A filter chip is a toggle button with aria-pressed; forced colors draws a 2px outline when on.
- Inside a FilterBar the chips are joined cells; they draw their focus ring inside the cell, so it never sits on a neighbour's line.
- Space or Enter toggles; Backspace, Delete or Enter on a remove button removes the value.
- A removed value moves focus to the next remove button, else the previous, else focusAfterEmpty.
- The remove button name is "Remove" plus the text; pass removeLabel for element children.
- An invalid value adds "(invalid)" in sr-only text.
- The remove glyph reaches 24px tall through its hit area.
Edge cases
13 edge cases
- Long label
- Never wraps; a string label ends in an ellipsis.
- One character
- About 28x28.
- No children
- An empty 28px button with no name; do not render empty chips.
- 25 chips in 160px
- A wrap group wraps; a strip never does, so a toolbar moves it to its own row.
- Right to left
- Label at the start, remove button at the end.
- Slow filter
- The chip flips at once; the list shows the wait. A chip never pends.
- Value the field rejects
- Fail wash and "(invalid)"; the value stays so it can be fixed or removed.
- Hold Backspace on remove
- Focus walks along the row; the last removal goes to focusAfterEmpty.
- Inside a form
- Toggle and remove are type="button"; they never submit.
- Ten clicks in a second
- Toggles ten times and ends in the right state.
- Forced colors
- Every chip keeps a 1px outline; pressed draws 2px; invalid draws it dashed.
- Pressed and disabled
- Keeps the darker edge and drops the fill, so the applied filter still shows.
- Touch
- Hover is guarded, so nothing sticks after a tap.
API
11 props
variant'secondary' | 'ghost' | 'outline'default'secondary'size'default' | 'sm' | 'xs'default'default'kindKindcountnumberpressed, defaultPressed, onPressedChangeBase UI ToggleonRemove() => voidremoveLabelstringdefault'Remove ' + textinvalidbooleanfocusAfterEmptyRefObject<HTMLElement | null>chevronbooleanChipGroup variant'wrap' | 'strip'default'wrap'
Tokens
19 tokens
--line-control- oklch(var(--ink-c) / 0.16)
--line-control-hover- oklch(var(--ink-c) / 0.32)
--elev-control- 0 1px 2px -1px oklch(var(--text-on-field-c) / calc(var(--shadow-alpha) * 0.6))
--surface-page- var(--paper)
--surface-hover- oklch(var(--ink-c) / 0.05)
--surface-selected- oklch(var(--ink-c) / 0.09)
--text-1- var(--ink)
--accent-wash- var(--surface-selected)
--accent-line- var(--ink)
--text-disabled- oklch(var(--ink-c) / 0.40)
--control-sm- 28px
--radius-soft- 6px
--radius-inner- 4px
--type-label- 400 14px/16px var(--font-sans)
--type-label-sm- 400 12px/16px var(--font-sans)
--dur-fast- 120ms
--dur-press- 60ms
--scale-press- 0.98
--text-2- oklch(var(--ink-c) / 0.80)