Badge
A compact status or label chip — 4 variants × 5 semantic colors × 3 real size tiers, with a dot, a leading icon, and loading.
- Status
- Since
0.1.0- Accessibility pattern
- non-interactive label
Last updated
Install
Add Badge from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/badgeThe same command installs the registry items it composes: @vegastack/spinner.
Usage
import { Badge } from "@/components/ui/badge";
<Badge intent="success" dot>
Active
</Badge>;Examples
Variants
The system's variant vocabulary, the same four words Button uses: solid (full-color
background with on-color text), soft (a tinted fill — the default), outline
(hairline border, no fill — the neutral/hued tag chip), and minimal (ink only, no
container at all).
Colors
Color is a semantic token (default, success, warning, destructive,
info) — never an arbitrary hex. Each color maps to design-system tokens across every
variant: solid (full fill), soft (tint), outline (hairline), and minimal
(ink only).
Sizes
Three real height tiers: sm 16px, md 20px, lg 24px. The dot and any
composed icon scale with the badge. sm is the dense-table chip — before this it was
md with 2px less horizontal padding, which is not a size.
States
Add a leading dot indicator, pass an icon to take the dot's place, or set loading
to show a spinner and aria-busy. Precedence runs loading → icon → dot.
On the solid variant the dot switches to bg-current, inheriting the on-color foreground so
it stays legible against the filled background.
Motion
Pass animateIn to pop the badge in (motion-pop-in) when its own appearance is the signal — a
status that just flipped, a label freshly applied. It's off by default: a badge rendered as
part of a static list (a table column, a filter chip row) must not pop every time its parent
re-renders or mounts.
Minimal — the dense-table treatment
minimal has no background, no border and no horizontal padding, so it sits flush in a
table cell rather than faking a pill. Because it has no container, the dot is on by
default — the shape is the second channel that keeps status off colour alone (WCAG
1.4.1). Pass dot={false} to drop it, or an icon to take its place.
Tag chips — bordered and outline
The Attio-teardown chip formula: bordered draws the matching-hue hairline on the soft
tint (crisper edges on white); variant="outline" is the fill-less neutral/hued tag.
Compose a +N overflow as a neutral outline badge — or reach for TagGroup, which does
it for you.
Playground
Mix variant, color, size, and the dot / loading / animate-in states, then copy the generated JSX.
<Badge>Active</Badge>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
animateIn | boolean | false | Opt-in mount animation (motion-pop-in, a scale + fade "arrival") for a
badge that appears in response to a real event — e.g. a status that just
flipped to "Verified", a freshly-applied label, or a badge toggled on by
a user action. **Default off**: a badge rendered as part of a static list
(a table column, a filter chip row) must not pop every time its parent
re-renders or mounts. Set it only where the badge's own appearance IS the
signal. |
bordered | boolean | false | Draw the matching-hue hairline border on the soft tint (the crisp
"chip" read on white surfaces). No-op on other variants. |
dot | boolean | — | Show a small leading dot indicator colored by intent. Ignored while
loading, and replaced by icon when one is given.
Defaults to true on variant="minimal" and false everywhere else: a
minimal badge has no container, so the dot is the only non-colour carrier of
its status (WCAG 1.4.1). Pass dot={false} to opt a minimal badge out. |
icon | React.ReactNode | — | Leading icon, rendered in the dot's place. Pass a lucide-react element
(or Icon) — it is sized by the badge's own [&_svg] rule, so do not set
size on it. Ignored while loading. |
intent | "default" | "destructive" | "info" | "success" | "warning" | 'default' | Semantic intent family. Maps to design-system tokens only — never an
arbitrary hex or color-mix value. default is the neutral muted badge. |
loading | boolean | false | Replace the leading content with a spinner and set aria-busy. Takes
precedence over icon and dot. |
render | useRender.RenderProp<Record<string, unknown>> | — | Replace the rendered element via Base UI render composition. Pass a
ReactElement or a render function. |
size | "lg" | "md" | "sm" | 'md' | Size tier — three real heights: sm 16px (dense tables), md 20px, lg
24px. The dot and any composed icon scale with the badge. |
variant | "minimal" | "outline" | "soft" | "solid" | 'soft' | Visual treatment — the system's variant vocabulary, shared with Button.
- solid: family fill with on-color text.
- soft: {family}-subtle tint + {family}-text (default).
- outline: hairline chip, no fill — the neutral/hued tag treatment.
- minimal: ink only — no fill, no border, no horizontal padding, and a
leading dot by default so status is never signalled by colour alone. |
Accessibility
- Renders a non-interactive
<span>by default — a badge is a label, not a control. - The
dotandloadingspinner are decorative (aria-hidden); the badge text carries the meaning, so never rely on color or the dot alone to convey status. loadingsetsaria-busy="true"while content is updating.- The loading spinner uses
animate-spin; it freezes underprefers-reduced-motionthrough the global reset, which every component inherits rather than restating. variant="minimal"turns thedoton by default: without a container, the dot is what keeps the status from resting on colour alone.- Use
renderto compose with a link when a badge needs to navigate, keeping the surrounding text as the accessible name.
| Contract | States tested |
|---|---|
| Behaviour | default, error, loading, success |
| Accessibility | busy |
| Visual | default, loading, error, success |