Chip
StableA small label on a thing, in four shapes (outline, solid, muted, dashed), with an optional icon and remove button.
Kinds of chips differ by shape, never by colour: a ring, a solid fill, a quiet tint and a dashed edge. Long text is cut with an ellipsis instead of breaking the row. Give it onRemove and it grows a small × button named “Remove <label>”.
Updated View source
Shapes and sizes
import { Chip } from '@rchursin/product/ui'
/** Four kinds, told apart by shape: a ring, a solid fill, a quiet tint and a dashed edge. Two sizes. */export default function ChipVariants() { return ( <div className="flex flex-col items-center gap-5"> <div className="flex flex-wrap items-center justify-center gap-2"> <Chip icon="tag">telemetry</Chip> <Chip variant="solid" icon="play"> Running </Chip> <Chip variant="muted" icon="pencil"> Draft </Chip> <Chip variant="dashed" icon="clock"> Not saved </Chip> </div> <div className="flex flex-wrap items-center justify-center gap-2"> <Chip size="xs">skill</Chip> <Chip size="xs" variant="solid"> Live </Chip> <Chip size="xs" variant="muted"> Paused </Chip> <Chip size="xs" variant="dashed"> Empty </Chip> </div> </div> )}Usage
import { Chip } from '@rchursin/product/ui'<Chip icon="tag">telemetry</Chip><Chip variant="solid">Running</Chip>Examples
Removable
Press × (or Tab to it and press Enter). The chip takes its button with it, so the example moves focus to the next chip and announces the change.
import { useRef, useState } from 'react'import { useAnnounce } from '@rchursin/product/lib/announce'import { Button, Chip } from '@rchursin/product/ui'
const initial = ['telemetry', 'cost', 'agent', 'docs']
/** * onRemove adds a small × button named "Remove <label>". Removing a chip takes its button with it, * so this example moves focus to the chip that took its place and says what happened. */export default function ChipRemovable() { const [labels, setLabels] = useState(initial) const list = useRef<HTMLUListElement>(null) const restore = useRef<HTMLButtonElement>(null) const announce = useAnnounce()
function remove(index: number) { const next = labels.filter((_, i) => i !== index) setLabels(next) announce(`Removed ${labels[index]}`)API
Chip props
| Prop | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | The label. Plain text keeps the remove button’s name automatic. |
icon | IconName | – | An icon before the label, 12 pixels, decorative. |
variant | 'outline' | 'solid' | 'muted' | 'dashed' | 'outline' | The shape. Solid for the one state that matters most; dashed for empty, pending or unsaved. |
onRemove | () => void | – | Adds a × button that calls this. Move focus somewhere sensible afterwards: the button is gone. |
removeLabel | string | – | Name for the × button, e.g. “Remove label skill”. Defaults to “Remove <children>” when children is a string or a number, otherwise just “Remove”. |
size | 'xs' | 'sm' | 'sm' | 20 or 24 pixels tall, with 11 or 12 pixel text. |
className | string | – | Extra classes on the chip. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves to each chip’s × button; the chip itself is not a stop. |
| EnterSpace | Removes the chip (calls onRemove). |
- A chip is plain text in a span. Only the × is interactive: a real <button> with its own name (“Remove telemetry”) and the shared focus ring.
- Pass removeLabel when the label isn’t a plain string; otherwise the button is only named “Remove”.
- The × is 16 pixels, under the 24 pixel minimum, so it relies on WCAG 2.2’s spacing exception: keep removable chips at least 8 pixels apart (gap-2), as every list in the kit does.
- Removing a chip doesn’t move focus by itself. Send it to the next chip, the previous one, or the field that adds chips.
Guidelines
Do
- Keep chip text to a word or two.
- Put chips in a list (<ul>) when there are several, so screen readers count them.
- Use the same shape for the same meaning across the product.
Don’t
- Don’t use a chip as a button or a link; use Button or a link.
- Don’t tell chip kinds apart by grey level alone.
In use
Related
- Status & priorityTask glyphs told apart by shape: a square that fills as work moves on, priority bars, assignee, labels and estimate.
- Verdict badgeKeep, fix, merge or cut: a mono badge where each verdict has its own shape, and the word is always there.
- ButtonThe lit-metal button in five finishes and five sizes, with icons, a loading state and shortcut hints.
- Fills & patternsSolid, hatched, outline and dashed: the second “colour” of a black-and-white interface, plus the dot-grid canvas.
Source
The component’s own files, exactly as they run on this site.
/** * Small pieces: avatars, chips, tooltip, empty state. */import { cloneElement, isValidElement, useEffect, useId, useRef, useState, type ReactElement, type ReactNode,} from "react"import { cx } from "../../components/cx"import { Icon, type IconName } from "../icons/Icon"import { computePosition } from "./popover"
/* ---------- Avatar: initials on a grey square (the brand square, softened) ---------- */
const tones = [