Gesso
Forms

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.

sm
lg

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.

always
hover
none

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.

ghost
outline

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.

PropTypeDefaultWhat it does
childrenReactNodeRequired. 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.
selectedbooleanfalseFills the chip to show a non-default value is applied: bg-fill on ghost, bg-tone-fill on outline.
iconBeforeComponentType<{ className?: string }>An icon component before the value, 16px at sm and 20px at lg. Ignored when mark is set.
markReactNodeAny 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 through asChild: a Dropdown menu trigger sets aria-haspopup="menu", a Popover or Combobox trigger sets aria-haspopup="dialog", and each sets aria-expanded and data-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 mark that 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 an aria-label that names the property and the value, like aria-label="Health: On track". A visible label beside the chip, like a <dt>, does not name the button. An icon from iconBefore and 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.
  • selected is a fill and nothing else. It sets no aria-pressed or other state, and forced colors repaints the fill, so put the value in the chip's text or its aria-label.
  • Focus shows a 2px ring on :focus-visible only, 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.
  • disabled sets 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 lg with the outline ring 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 multiple to pick several values from a list long enough to search. Its own ComboboxButton reads as a form field, so use that in a form and this chip in a band of properties.

Install

shadcn add @gesso/chip-trigger

It installs Button with it, and @gesso/filter-chip installs it, so adding Filter chip gets you both.

On this page