Chip

Filters a list, or holds a value someone can remove.

Examples

Filter conversations by status

Conversations2 of 6
cnv_8f2brefund-agentWaiting on you
cnv_8f26billing-agentWaiting on you

Values you can remove

queue:billingtriagefast

Styles

  • secondarydefault

  • ghost

  • outline

Group

  • queue:billingqueue:refunds

    wrapdefault

  • strip

Sizes

  • embed-small

    defaultdefault

  • queue:billing

    xs

States

  • rest

  • hover

  • focus

  • active

  • on

  • disabled

  • queue billing(invalid)

    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'
kind
Kind
count
number
pressed, defaultPressed, onPressedChange
Base UI Toggle
onRemove
() => void
removeLabel
string default 'Remove ' + text
invalid
boolean
focusAfterEmpty
RefObject<HTMLElement | null>
chevron
boolean
ChipGroup 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)

Install

For your coding agent

GitHub

Please star me pookie

141

A new design system every week

Leave your email before you copy the prompt.