Gesso
Forms

Filter chip

Interactive filter dropdown. Selected and open states sit on the chip trigger.

Filter chip is a filter for a toolbar: a chip that opens a Dropdown menu of options and shows the current choice on its face. Its button is Chip trigger, at the lg size by default, with the brand ring and the caret always out, and selected fills it while a filter is applied, so a row of filters shows which ones are on. Chip is the label that opens nothing.

Usage

import {
  FilterChip,
  FilterChipContent,
  FilterChipRadioGroup,
  FilterChipRadioItem,
  FilterChipTrigger,
} from '@/components/ui/filter-chip';

<FilterChip>
  <FilterChipTrigger>{labels[range]}</FilterChipTrigger>
  <FilterChipContent>
    <FilterChipRadioGroup value={range} onValueChange={setRange}>
      <FilterChipRadioItem value="7d">Last 7 days</FilterChipRadioItem>
      <FilterChipRadioItem value="30d">Last 30 days</FilterChipRadioItem>
    </FilterChipRadioGroup>
  </FilterChipContent>
</FilterChip>

FilterChip is the Dropdown menu root, so it takes open, onOpenChange, and modal as that does. You hold the value, and the chip's label follows it. The rows are Dropdown menu's rows under new names: FilterChipLabel, FilterChipGroup, FilterChipItem, FilterChipSeparator, FilterChipCheckboxItem, FilterChipRadioGroup, and FilterChipRadioItem. There are no submenu or shortcut parts.

One choice or several

Radio rows pick one value, like a date range, and close the menu when picked. Checkbox rows turn facets on and off, and the menu stays open while you toggle them, so setting several facets is one visit. Pass closeOnSelect to a checkbox row that should close the menu.

Show that a filter is on

Pass selected while the filter is narrowing the results: for radio rows when the value is not the default, and for checkbox rows when any facet is on. The chip fills with bg-tone-fill, the same quiet tone fill as a Chip. Put the value in the label too, like "Status: Open", because the fill is the only other sign.

Groups take a FilterChipLabel heading, a FilterChipSeparator sets them apart, and a plain FilterChipItem runs an action, like a row that resets the filter.

The ring and the wash follow the look

The chip takes the primary tone, like Chip's outline, rather than Button's outline. The ring is border-tone/(--alpha-32), hover and open wash it with bg-tone-fill-hover, and the label stays text-fg in every state, at regular weight. Its corners stay at 8px in every look, so a look that rounds buttons into pills, like ICS, does not reshape it, and it does not grow or move on hover. The caret is a filled Phosphor CaretDown.

The menu opens 4px below the chip, lined up with its leading edge, and is at least 192px wide.

In a tab row

size="sm" sets the chip at 28px with text-xs and 16px icons, Chip trigger's sm metrics, with the same ring, fill, and caret. At that height it sits in a row of header tabs without its bottom edge landing on the header's rule, so a board or a roadmap can keep its filters beside its tabs. lg stays the size for a toolbar of filters.

<FilterChip>
  <FilterChipTrigger size="sm" selected={status !== 'all'}>
    {statusLabel}
  </FilterChipTrigger>
  <FilterChipContent>{/* rows */}</FilterChipContent>
</FilterChip>

Props

The root, the content, and the rows take the props of their Dropdown menu parts, plus these.

PropTypeDefaultWhat it does
size"lg" | "sm""lg"On FilterChipTrigger. 40px for a toolbar, or 28px to sit in a tab row.
selectedbooleanfalseOn FilterChipTrigger. Fills the chip with bg-tone-fill to show the filter is on.
iconBeforeComponentType<{ className?: string }>On FilterChipTrigger. A 20px icon before the label. Ignored when mark is set.
markReactNodeOn FilterChipTrigger. Any node before the label, like a dot. Wins over iconBefore.
align"start" | "center" | "end""start"On FilterChipContent. Lines the menu up with the chip's leading edge.
sideOffsetnumber4On FilterChipContent. The gap between the chip and the menu, in pixels.

FilterChipTrigger takes Chip trigger's props without variant and caret, which are fixed at outline and always. To change them, put a Chip trigger under a Dropdown menu yourself.

Accessibility

  • The chip is a native <button> with aria-haspopup="menu" and aria-expanded, and the open menu takes the chip as its label, so the menu is named by the chip's text. Enter, Space, or ↓ opens it and focuses the first row.
  • In the menu the keys are Dropdown menu's: ↑ and ↓ move between rows and stop at the ends, Home and End jump to the first and last, typing a letter moves to the next row that starts with it, and Enter or Space picks. Escape closes the menu and puts focus back on the chip.
  • Radio rows are menuitemradio and checkbox rows are menuitemcheckbox, each with aria-checked, so the check on the right is read out as well.
  • selected is a fill and sets no ARIA state, and forced colors repaints the fill, so the chip's text is what tells a screen reader, or someone in Windows high contrast, that the filter is on. A chip that reads "Status: Open" says it, and one that reads "Status" does not.
  • FilterChipLabel is plain text that the arrow keys skip, and it is not tied to its group, so give the FilterChipGroup an aria-label when the heading matters.
  • Focus shows a 2px ring in the contrast ink on :focus-visible only, and in forced colors a transparent outline that the system paints.
  • The chip is 40px tall at lg and 28px at sm, and at least 44px tall on a touch screen at either size, and it does not grow wider. The rows are 32px and do not grow.
  • modal is on by default, so while the menu is open the page behind it ignores the pointer and is hidden from assistive tech.
  • Under reduced motion the chip changes color without a transition, and the menu opens and closes without its fade.

When to use something else

  • Chip trigger when the chip opens a picker other than a filter menu, like a combobox, a popover, or a calendar, or for a band of properties that share one quiet look, with the ghost variant, the sm size, and a caret that waits for hover.
  • 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, like the number of results, and for a value shown inside a button, because it renders a <span> that nests there.
  • 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. This menu grows with its rows and does not scroll.

Install

shadcn add @gesso/filter-chip

It installs Chip trigger and Dropdown menu with it.

On this page