Skip to content

Chip

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/chip.json
See it livePlain, with a count, removable, in every tone, and switching alone and in a group.→

The difference from a Badge is whether something happens when you click it. If something does, that part is a real <button> with a real name — not a decorative cross, and not a <span> with a click handler. A chip is acted on in two ways, taken away or switched, and each has its own shape.

Removable. Every product wrote the removable tag and every one made the cross a <span>, which the keyboard cannot reach and a screen reader does not announce.

<Chip onRemove={() => drop(tag)} removeLabel={t('remove')}>
{tag}
</Chip>

removeLabel is yours, and required: the two props travel together in the type, so a chip that can be removed but not named will not compile. The remove click does not reach what the chip sits in — removing a tag should not also open the row it was on.

Switchable. kilna drew twenty-three switchable chips by hand, in three sizes and five recipes, and not one of them told a screen reader whether it was on. Give a chip pressed, defaultPressed or onPressedChange and it is Base UI’s Toggle: a <button> with aria-pressed. “On” is the accent fill in semibold, drawn from the same attribute that announces it, so the look and the announcement cannot disagree.

<Chip pressed={onlyScored} onPressedChange={setOnlyScored}>
Scored
</Chip>

Never both. A chip that switches and also holds a cross is a button inside a button: the cross is a nested control no reader can reach on its own, and a press on it would switch the chip as well. The type refuses the combination.

A row of switches is a ChipGroup. Base UI’s ToggleGroup underneath: the row is one stop on the Tab key and the arrows walk the chips — a filter bar of eight kinds is not eight Tab presses long. multiple decides whether pressing one lets go of the others. The value is an array in both modes, which is Base UI’s shape; a single group holds at most one.

<ChipGroup aria-label={t('kind')} value={kinds} onValueChange={setKinds} multiple>
<Chip value="song" count={42}>Song</Chip>
<Chip value="clip" count={12}>Clip</Chip>
<Chip value="short">Short</Chip>
</ChipGroup>

For one of a few that must always hold a choice — a theme, a view — reach for SegmentedControl instead: a toggle row lets the last pressed chip go.

Tones are the Badge’s. accent, good, warn, bad and info are the same fill-and-ink pairs Badge uses, so a chip and a badge in one row agree about what a tone looks like. dashed is the outline with a broken border, for a chip that stands for something not there yet — “+ Tag”, “Any kind”.

A switch grows its hit area, not itself. A chip is 21 pixels tall by design; as a switch it carries target-min, so the pointer target reaches the 24px floor while the chip stays the size it is.

Props

Read from the types in ui/chip.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

Chip

PropTypeDefault
variantoutline | dashed | soft | accent | good | warn | bad | info'outline'
countnumberShown after the label, for a count.
classNamestring
childrenReactNode
onRemovewith removeLabel() => voidCalled when the cross is pressed. Giving it is what makes the chip removable; the press does not reach a clickable parent.
removeLabelwith onRemovestringThe accessible name of the cross, for example "Remove tag". Required with onRemove, and there is no default word, so the product can translate it.
pressedbooleanWhether it is on. Controlled; pair it with onPressedChange.
defaultPressedbooleanWhether it starts on, for a chip nobody else needs to know about.
onPressedChange(pressed: boolean) => voidCalled with the new state when the chip is switched. Giving it makes the chip a switch.
valuestringWhich member of a ChipGroup this is. Inside a group the group owns the state, and pressed is not read.
disabledbooleanStops the chip being switched, and dims it. Only a switch can be disabled.

And every prop of React's HTMLAttributes<HTMLElement> except onSelect, defaultValue, color, passed on.

ChipGroup

A row of chips that switch together.

PropTypeDefault
aria-labelstringWhat the row of chips is: "Kind", "Status". A group without a name is a row of buttons a reader meets one at a time with no idea what they choose between.
aria-labelledbystringIdentifies the element (or elements) that labels the current element.
valuereadonly string[]The values of the chips that are on. An array in both modes, because that is Base UI's shape and a single mode is a group that holds at most one.
defaultValuereadonly string[]The values that start on, when the group keeps its own state.
onValueChange(value: string[]) => voidCalled with the values that are on after a chip is pressed. Always an array, in both modes.
multiplebooleanfalseSeveral on at once - filters - rather than one at a time - a kind.
disabledbooleanDisables every chip in the group.
childrenrequiredReactNode

And every prop of React's HTMLAttributes<HTMLDivElement> except defaultValue, onChange, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 17 rules applied and passed, and none failed. The accessibility report has every item.

Left to a person, where axe could not decide: color-contrast.

History

Each release that changed ui/chip.tsx - the file a product copies - newest first.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.39.0
    • fix draw only promised inks on fills 6ccd89f
  3. v0.33.0
    • feat switch alone and in a group, in the tones of Badge c6161b1
  4. v0.28.0
    • feat a floor for pointer targets, and a way to reach it 2d397f5
  5. v0.7.0
    • feat budget what a primitive costs and forbid words of its own 8163f87
  6. v0.5.0
    • feat add the nine base primitives 81a2fd9