Gesso
Forms

Chip

Inline filter and selection chips with optional icon and dismiss.

Chip is a soft label for a value, like a picked option, an applied filter, or a category. It takes a leading icon and, with onRemove, a small dismiss button, and it opens nothing, so the dismiss button is the only part that takes a click. Chip ships in @gesso/badge beside Badge, and its corners stay at 8px in every look, so a look that rounds its buttons into pills, like ICS, does not reshape it.

Default
Outline
Ghost
With icon
Outline with icon

Usage

import { Tag } from '@phosphor-icons/react';
import { Chip } from '@/components/ui/badge';

<Chip iconBefore={Tag}>Design</Chip>

Chip comes from the badge file, because it installs with Badge. iconBefore takes an icon component, so set it from a client component, as with Button. A Server Component cannot pass a component to a client one.

Three weights that follow the look

default fills the chip with bg-tone-fill and sets the label in text-fg. outline draws a ring, border-tone/(--alpha-32), around a text-tone label, and ghost is the text-tone label alone. A chip is the primary tone, so all three follow the brand color. None of them changes on hover or focus, because the chip itself is not a control.

To make a chip the second hue, such as a workspace name, set data-tone="support" on it. A chip sets its own tone first, and an attribute you pass wins, so this is a recipe and not a prop:

<Chip data-tone="support">Northwind</Chip>

In the default Ink look both tints are grays, and the fill takes on the warmth or coolness of the paper. Washi, ICS, and Ramson keep the hue of their own tints: Washi fills with blossom and rings in wave blue, ICS fills orange and rings violet, and Ramson fills blush and rings mint, and each tint deepens in dark mode. The ring is as thick as the look's UI weight asks, 1px at 400 and 2px at 600, and the label takes that weight too.

A chip is 29px tall in text-sm, with a 14px icon at 64% opacity. The label stays on one line, and a chip given a max-w-* ellipsizes it.

Removable values

Pass onRemove and the chip gets a trailing dismiss button that calls it. The chip does not remove itself, so the parent holds the list and filters the value out. This is the usual way to show what a Combobox with multiple has picked.

const [teams, setTeams] = useState(['Design', 'Engineering']);

<ul aria-label="Selected teams" className="flex flex-wrap gap-2">
  {teams.map((team) => (
    <li key={team}>
      <Chip onRemove={() => setTeams((t) => t.filter((x) => x !== team))}>
        {team}
      </Chip>
    </li>
  ))}
</ul>

The dismiss button is named "Remove" and the label, so a chip that reads Design has a button named "Remove Design". When the label is not a plain string the name falls back to "Remove", so pass removeLabel with the value in it. A click on the dismiss button stops there, so a chip inside a clickable row does not also fire the row. When a chip goes, its button goes with it, so move focus yourself, to the next chip's dismiss button or to the control that adds values, as the demo does.

  • Design
  • Engineering
  • Product

Category color

When the color belongs to the value, like a priority that is orange at High or a "Beta" label, give the chip a color from the category palette. It wins over the primary tone, so High stays orange whether the look's primary is ink, wave, or violet. default takes the hue's fill, label, and hairline, outline the label and hairline, and ghost the label alone.

Chip

Default
Outline
Ghost

Chip with a color

High
Medium
Design

Outline and ghost

Beta
Pro

CategoryTag, in a cell

grayredorangeamberyellowgreenbluepurplepink
<Chip color="orange">High</Chip>
<Chip color="blue" variant="outline">Beta</Chip>

In a table cell, where a chip is a little smaller, use CategoryTag from Cell fields, which is Badge with a color in text-xs and takes the same onRemove and removeLabel.

catChipStyle mixes the category hue into the page's background and ink with their own color stripped out, so a tag reads on any paper without taking on the brand.

Part of the tagMix
Fill32% of the hue into the background (--alpha-32)
Label48% of the hue into the ink (--alpha-48)
Hairline48% of the hue into the background (--alpha-48)

The weights are tuned for AA label contrast on the fill, for every hue, in light and dark, on base, warm, and cool paper. Warm or cool paper lends each hue a little of its chroma, and gray takes the paper's hue, while a look's brand colors never reach a tag.

Disabled

disabled dims the chip to 64% opacity, stops the pointer, and disables the dismiss button, so the chip cannot be removed by click or by keyboard.

Disabled
Disabled with dismiss

Props

Chip takes every <div> attribute, plus these.

PropTypeDefaultWhat it does
childrenReactNodeRequired. The label. A string also names the dismiss button.
variant"default" | "outline" | "ghost""default"The weight: a tone fill, a tone ring, or the text-tone label alone.
color"gray" | "red" | "orange" | "amber" | "yellow" | "green" | "blue" | "purple" | "pink"A category hue, which wins over the primary tone.
iconBeforeComponentType<{ className?: string }>An icon component drawn before the label at 14px and hidden from assistive tech.
onRemove() => voidRenders a dismiss button that calls it. The parent removes the value.
removeLabelstring"Remove {children}"The dismiss button's accessible name. The default is plain "Remove" when the label is not a string.
disabledbooleanfalseDims the chip, ignores the pointer, sets aria-disabled, and disables the dismiss button.

Chip renders a <div> rather than a <span>, so the dismiss <button> is valid inside it. It also means a Chip cannot go inside a button, so a value shown inside a Chip trigger is a Badge.

Accessibility

  • The chip is a plain <div> with no role, so a screen reader reads its label as text, and it takes no focus, so a row of chips adds no tab stops of its own. Put a set of them in a list with an aria-label, so the group and its count are announced.
  • The dismiss button is a native <button type="button">, so Tab reaches it and Enter or Space removes the chip. Its name is "Remove" and the label, or removeLabel, and its X icon is hidden.
  • Focus on the dismiss button shows a 2px outline in the contrast ink, offset 1px, on :focus-visible only. It is a real outline, so it stays visible in forced colors.
  • Chip does not move focus when one is removed, so focus drops to the page unless the parent moves it.
  • The dismiss button is 18px square and does not grow on a touch screen, so it is a small target on a phone. Offer another way to clear values too, like the picker the chips came from.
  • disabled sets aria-disabled on the chip and the native disabled on its dismiss button, which takes the button out of the tab order.
  • The variants differ only in weight, and a category hue only groups values, so color carries no meaning that the label does not. Keep the value in the text.
  • The dismiss button's hover color fades over a short transition, and reduced motion does not turn it off. Nothing on the chip moves.

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.
  • Filter chip for a filter in a toolbar that opens a menu of options and shows the current choice on the chip.
  • Category colors for a standalone label such as "Beta" or "Pro", and for a value that keeps its own hue in every look, through CategoryTag.
  • Badge for a small mark inside a chip, like a count, and for a value shown inside a button, because it renders a <span> that nests there.
  • 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. Show the picks as removable Chips beside it, or as Badges inside a Chip trigger.

Install

shadcn add @gesso/badge

Chip installs with Badge and CircleBadge, and imports from @/components/ui/badge.

On this page