Chip trigger
The chip button surface on its own, ready to sit under any picker root.
Chip trigger is the chip-shaped button on its own. It opens nothing and holds no state, so it goes
under whichever picker a value needs, through that picker's trigger and asChild: a
Dropdown menu, a
Combobox, or a Popover
holding a Calendar, the way
Date picker is built. It uses no brand tint by default, so
a band of editable properties can open four different pickers and still read as one set.
Filter chip is this button with the brand ring, already wired
to a dropdown.
- Health
- Assignees
- Labels
- Target
Usage
import { CalendarBlank } from '@phosphor-icons/react';
import { ChipTrigger } from '@/components/ui/chip-trigger';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
<Popover>
<PopoverTrigger asChild>
<ChipTrigger size="sm" caret="hover" iconBefore={CalendarBlank}>
End of month
</ChipTrigger>
</PopoverTrigger>
<PopoverContent align="start">{/* … */}</PopoverContent>
</Popover>The picker's trigger hands its open state, ARIA, and handlers to the chip through asChild, and
the chip spreads every prop onto its <button> and forwards its ref, so any Radix trigger can drive
it. iconBefore takes an icon component, so set it from a client component, as with
Button.
One surface under any picker
The chip looks the same whatever opens below it. In the band above, health opens a dropdown, assignees and labels open a searchable combobox that takes several values, and the target date opens a calendar in a popover. Each of those is a small composition around Chip trigger rather than a kit component, because the value and the picker change from property to property while the surface does not.
A popover that holds a picker hands its padding to the picker. PopoverContent is a 288px panel
with p-4, sized for prose, so give it className="w-auto p-0", the same override Date picker
uses, rather than a smaller p-*. ComboboxContent already sets p-0 and matches the trigger's
width, with a 224px minimum.
Values that are more than text
A property can hold several values, like assignees or labels, and the chip shows them as tokens.
Use Badge for a token inside the trigger, never Chip. Badge
renders a <span> and nests inside a button, while Chip renders a <div> that can hold its own
dismiss button, so inside a trigger it is invalid markup and puts a button inside a button. Show
values as Chips outside the trigger, where each one can be removed,
and as Badges inside it.
For people, use Avatar with size="xs", which sets the
20px circle and its initials together. Sizing the avatar through className alone leaves the
initials at text-sm, too large for the circle. The demos slim their value badges with
px-1.5 font-medium, because a Badge on its own is sized to stand alone.
How many tokens show before the rest collapse into a count is up to you. Slice the array and show
the remainder as one more Badge, like +2. A Combobox with multiple keeps its list open across
picks, and you hold the array.
Long values truncate
The value sits in its own row that can shrink, so a chip with a width cap ellipsizes the value and keeps the caret in place. Without a cap the chip sizes to its content.
Two sizes
lg is the default and the filter chip's size: 40px tall, text-sm, 20px icons, 12px of padding
on each side, and an 8px gap. sm is for a property band: 28px tall, text-xs, 16px icons, and a
6px gap, quiet enough to sit several to a column without reading as a toolbar. sm is padded 6px on
the left and 8px on the right, because a borderless chip has no ring to hold its value off the edge
and a band reads better with the value close to flush, and the caret keeps the room on the right.
On a touch screen both sizes grow to at least 44px tall. They do not grow wider.
The caret can wait
caret is always by default, as on the filter chip. hover hides the caret until the pointer is
over the chip, the chip has keyboard focus, or its picker is open, which keeps a band of properties
calm until someone reaches for one. The hidden caret keeps its space, so showing it never shifts the
row. none drops it, for a chip whose picker is clear from something else.
A mark or an icon
iconBefore takes an icon component, as in iconBefore={CalendarBlank}, and draws it at the
size's icon size. mark takes any node, for a status mark or a dot that is not an icon.
import { CircleBadge } from '@/components/ui/badge';
<ChipTrigger size="sm" mark={<CircleBadge data-tone="success" className="bg-tone" />}>
On track
</ChipTrigger>Set both and mark wins, because the two take the same place. A status mark is the one place on
the chip where color is expected. The health mark below is
Health face, inked with the system's success, warning,
and danger colors rather than the brand.
Ghost by default, outline for filters
variant="ghost" is the default and uses no brand tint. The chip is transparent with a clear
border, and hover and open wash it with bg-fill, the same wash a ghost Button hovers with, so a
column of property chips reads as values rather than a stack of buttons. outline is
the filter chip: a primary-tone ring, border-tone/(--alpha-32), with a bg-tone-fill-hover wash on
hover and while open. The ghost border is a real border in a clear color, so both variants have the same box and
only their color differs. In both, the corners stay at 8px in every look and the label is set at
regular weight.
selected fills the chip inside its variant's own palette, so a ghost chip never reaches for a
brand tint: ghost fills with bg-fill and hovers bg-fill-hover, and outline fills with
bg-tone-fill. It means a non-default value is applied, like a filter that is on, rather
than that the chip has a value. A property chip always has a value, so leave selected off there,
or the whole band lights up. Pick a status below to see it.
Every data-* attribute reaches the button, so you can tone a chip the way Badge's tag variant is
toned, from a data-* attribute or an inline style. Leave data-tone for the kit, which uses it
to set a hue, and keep the mapping from your own value to a color on your side.
Props
Chip trigger takes every <button> attribute, plus these.
| Prop | Type | Default | What it does |
|---|---|---|---|
children | ReactNode | Required. The current value, or a placeholder. | |
size | "sm" | "lg" | "lg" | 28 or 40px tall, and at least 44px tall on a touch screen. |
variant | "ghost" | "outline" | "ghost" | ghost washes bg-fill and uses no brand tint. outline adds a primary-tone ring and a bg-tone-fill-hover wash. |
selected | boolean | false | Fills the chip to show a non-default value is applied: bg-fill on ghost, bg-tone-fill on outline. |
iconBefore | ComponentType<{ className?: string }> | An icon component before the value, 16px at sm and 20px at lg. Ignored when mark is set. | |
mark | ReactNode | Any node before the value, like a status mark. Wins over iconBefore. | |
caret | "always" | "hover" | "none" | "always" | hover shows the caret on hover, on keyboard focus, and while open, without shifting the row. none drops it. |
type | "button" | "submit" | "reset" | "button" | As on <button>, except it never submits a form unless you set "submit". |
Accessibility
- It renders a native
<button type="button">and forwards its ref, and on its own it opens nothing and carries no popup state. The picker's trigger adds that throughasChild: a Dropdown menu trigger setsaria-haspopup="menu", a Popover or Combobox trigger setsaria-haspopup="dialog", and each setsaria-expandedanddata-state. - Enter and Space press it, as on any button, and under a Dropdown menu ↓ opens the menu too.
- Its accessible name is its text, plus the name of anything in
markthat has one. Health face is an image named "Health On track", so a health chip with no label of its own is read as "Health On track On track". Give the trigger anaria-labelthat names the property and the value, likearia-label="Health: On track". A visible label beside the chip, like a<dt>, does not name the button. An icon fromiconBeforeand the caret add nothing to the name, and neither does a CircleBadge, which is hidden, so a dot only works beside text that says the status. selectedis a fill and nothing else. It sets noaria-pressedor other state, and forced colors repaints the fill, so put the value in the chip's text or itsaria-label.- Focus shows a 2px ring on
:focus-visibleonly, offset from the edge, in the contrast ink, and in forced colors the chip keeps a transparent outline that the system paints, as Button does. - With
caret="hover"the caret also shows on keyboard focus and while the picker is open, so a keyboard user sees what a mouse user sees. disabledsets the native attribute, so the chip leaves the tab order, drops to 64% opacity, and ignores the pointer.- On a touch screen both sizes are at least 44px tall. The width follows the content, so a short value still makes a narrow target.
- Under reduced motion the color change and the caret's fade happen without a transition.
When to use something else
- Filter chip for a filter in a toolbar that opens a menu of
options and shows the current choice on the chip. It is this button at
lgwith theoutlinering and the caret out, already wired to a Dropdown menu. - Chip for a value that opens nothing, like a picked option or an applied filter, with an optional dismiss button.
- Badge for a read-only status or count, and for the value
tokens inside this chip, because it renders a
<span>that nests in a button. - Category colors for a value that keeps its own
hue in every look, through
CategoryTag. - Toggle group when there are only a few options and they should all stay in view, one press each, with no menu to open.
- Combobox with
multipleto pick several values from a list long enough to search. Its ownComboboxButtonreads as a form field, so use that in a form and this chip in a band of properties.
Install
shadcn add @gesso/chip-triggerIt installs Button with it, and @gesso/filter-chip installs it, so adding Filter chip gets you
both.