Pill
Compact, rounded tag with color variants, sizes, an optional leading icon, and an optional remove control.
Preview
<Pill label="Wetlands" variant="success" removable={false} /> Variants
<Pill label="Default" variant="default" removable={false} />
<Pill label="Primary" variant="primary" removable={false} />
<Pill label="Success" variant="success" removable={false} />
<Pill label="Warning" variant="warning" removable={false} />
<Pill label="Danger" variant="danger" removable={false} />
<Pill label="Info" variant="info" removable={false} /> Sizes
<Pill label="Extra small" size="xs" removable={false} />
<Pill label="Small" size="sm" removable={false} />
<Pill label="Medium" size="md" removable={false} />
<Pill label="Large" size="lg" removable={false} /> Shape
Pills are squared by default — a 4px corner (--radius-sm),
the same one badge uses, so chips across a table or a
filter bar read as one quiet family. shape="round" opts into the fully
rounded capsule (--radius-pill) where the tag is the emphasis rather
than the data beside it. Both follow the tier-2 radius roles, so a squared-off
brand re-points one role and every pill follows.
<Pill label="Squared" variant="primary" removable={false} />
<Pill label="Rounded" variant="primary" shape="round" removable={false} /> <Pill label="Extra small" size="xs" shape="round" removable={false} />
<Pill label="Small" size="sm" shape="round" removable={false} />
<Pill label="Medium" size="md" shape="round" removable={false} />
<Pill label="Large" size="lg" shape="round" removable={false} /> Icon & removable
Pass a leading icon via the icon slot. With removable (the default), a × button emits a removed event and removes the pill from the DOM.
<Pill label="Tagged" variant="primary" removable={false}>
<svg slot="icon">…</svg>
</Pill>
<Pill label="Wetlands" variant="success" />
<Pill label="Overdue" variant="danger" /> Custom colors
variant covers the six system intents and is deliberately closed —
success means succeeded, not "green". A project's own vocabulary is an open set the
hub can't enumerate: permit types, survey stages, species, funding sources. That's what
category is for.
Pass a category name and the pill emits data-category. Your theme scopes a
three-step ramp to that name; the pill reads steps 2 / 6 / 11 for fill, border and
text — the same triple every built-in variant uses, so your category renders as a native member
of the set rather than a foreign object.
Pick a scale, don't pick colors. Tier 1 ships the whole Radix palette — all 25 chromatic hues and all 6 neutral temperatures, twelve steps each, in light and dark. Point the three names at one of them and you're done — no hex anywhere:
<Pill label="Permit expired" category="permit-expired" removable={false} />
<Pill label="Awaiting survey" category="awaiting-survey" removable={false} />
/* in your <project>-theme.css — one block per category */
[data-category="permit-expired"] {
--category-2: var(--color-iris-2);
--category-6: var(--color-iris-6);
--category-11: var(--color-iris-11);
} Start with iris, gold or plum. They carry
no borrowed meaning: no semantic role points at them, and the hub's default chart series doesn't
seat them. Eight scales are spoken for — grass (brand),
blue (info), green (success), yellow (warning),
red (danger), copper (AI), orange (accent) and
gray (chrome). Reusing one makes a category look like a status it isn't, which is
worse than leaving it neutral.
A second, softer conflict: crimson, cyan, indigo,
teal and violet are seated in the hub's default
--color-background-dataviz-* series, so a category in one of those will match a chart
line on the same page. That's a resemblance, not a false status — take it if the hue is right and
the page has no charts.
There used to be a middle rung here and there isn't any more. When tier 1 shipped
eleven scales, reaching the rest meant @import '@radix-ui/colors/purple.css' in your
own theme. Since 2026-08-17 the whole palette is generated into tier 1, so
--color-purple-2/6/11 is already declared and the import is dead weight.
Need a brand color that isn't a Radix hue at all? Only then reach for
radix-ui.com/colors/custom,
which fits your color to a 12-step perceptual scale; copy steps 2, 6 and 11 and rename the
--accent- prefix.
Both rungs exist to avoid the same thing: a fill and a text color chosen independently. That is how dark purple ends up on near-black, and nothing in the build will tell you.
Know where the line is. Step 11 is tuned to APCA, not WCAG 2, so on step 2 it
sits at the AA boundary rather than above it. Measured across all 26 chromatic scales
tier 1 ships, six miss 4.5:1 for normal text: orange 4.25,
teal 4.34, yellow 4.43, amber 4.43, jade 4.43
and green 4.49. cyan lands exactly on 4.50. The rest pass, most of them
by a hair — blue 4.53, lime 4.56, grass 4.82 — and the
widest margin in the palette is violet at 5.86. The three starters above sit near the
top of that range: violet-adjacent iris 5.74, plum 5.69,
gold 5.60.
This is a property of the scale, not of this mechanism — the built-in warning variant
measures 4.43 and always has, and success joined it at 4.49 when it moved off lime.
Step 12 clears AA on every one of the 26 at 10.44:1 or better (the floor is
lime),
so a category carrying real weight should point --category-11 at step 12 instead; the
cost is a visibly darker label than the semantic pills beside it. Which step the system settles on
is a question for the contrast pass, not one to answer per component.
The three --category-* names are not hub tokens — your theme
declares them, the component reads them, which is the inverse of every other token in the kit.
That's deliberate: the mechanism is the hub's, the vocabulary is your project's. The
values you point them at should still be hub tokens, which is the whole argument above.
A category with no ramp defined, or a typo in the name, falls back to the default pill rather
than losing the declaration; and because the ramp arrives as one block, fill and text can't be
set independently.
API
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Required. Text shown inside the pill. |
variant | 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'default' | Color intent — drives background, text, and border. |
removable | boolean | true | Shows a remove (×) button. |
size | 'xs' | 'sm' | 'md' | 'lg' | 'md' | Pill height and text size. |
category | string | — | Project vocabulary this pill labels — emits data-category for a spoke-defined color ramp. See Custom colors. |
shape | 'default' | 'round' | 'default' | Corner language. Composes with every variant and size. |
Slots
| Prop | Type | Default | Description |
|---|---|---|---|
icon | slot | — | Leading inline SVG, rendered left of the label. |
Events
| Prop | Type | Default | Description |
|---|---|---|---|
removed | CustomEvent | — | Bubbles when the remove button is clicked; the pill then removes itself from the DOM. |
Accessibility
- The remove button has
aria-label="Remove"; the leading icon isaria-hidden. - Remove click
stopPropagation()s so it won't trigger a click on a surrounding interactive container. - Color intent is reinforced by the label text — color is never the only signal.