Badge
A static, inline label for a status, count, or short piece of metadata.
@garn/ui/badgeOn this page
Default
Tone + appearance defaults.
Tone × appearance
Every tone rendered against every appearance. Hover a cell for the data attributes it emits; click the copy icon for that combination's JSX.
Sizes
xs · sm · md · lg — scales font, padding, and slot icons together.
Shapes
sharp · rounded · pill.
Status
Dot, pulse, loading, and progress affordances.
Count
A trailing count, rendered by Counter (caps at 99+).
Icons
Leading/trailing icons; an icon-only badge needs an aria-label.
Custom Color
A custom color with automatic foreground contrast.
Truncation
Only the label ellipsizes; the full text stays in the title.
Compound
Explicit Badge.Leading / .Label / .Trailing for full control.
As Child
Renders as a link via asChild, keeping the badge styling.
Plan Badge
Badge.Separator (dot / line) divides inline segments — a status·action pill and a metadata divider. The action is a composed link; Badge stays non-interactive.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add badge- Registry
badge- Deps
@radix-ui/react-slotclass-variance-authoritylucide-react- Registry deps
utilsuse-clipboarduse-announcespinnercounter
Import
import { Badge } from "@garn/ui/badge";Props
The component's public props and their types.
appearance- Description
- No description.
- Type
"framed" | "outline" | "soft" | "solid" | "subtle"- Default
soft
shape- Description
- No description.
- Type
"pill" | "rounded" | "sharp"- Default
rounded
size- Description
- No description.
- Type
"lg" | "md" | "sm" | "xs"- Default
md
tone- Description
- No description.
- Type
"brand" | "danger" | "info" | "neutral" | "success" | "warning"- Default
neutral
asChild- Description
- Render the single child element as the root (via Radix `Slot`) — e.g. an `<a>` wearing the badge styling.
- Type
boolean- Default
false
avatar- Description
- A leading avatar (sugar for a small Avatar in the leading slot).
- Type
React.ReactNode
color- Description
- Custom token reference or hex/rgb value; overrides `tone`. A solid fill gets a luminance-checked foreground.
- Type
string
copyable- Description
- Add a trailing copy-to-clipboard button — Badge's only interactive affordance.
- Type
boolean- Default
false
copyValue- Description
- Value copied by the `copyable` button (defaults to the label text).
- Type
string
count- Description
- A trailing count, rendered by the Counter component (not a plain number): it caps at 99+, rolls its digits on change, and announces the true count to screen readers. Tone and size follow the badge; the count's appearance inverts against the badge's so it always contrasts.
- Type
number
dot- Description
- Show a status dot in the leading slot.
- Type
boolean- Default
false
leading- Description
- Fixed-width content before the label — an icon, dot, or avatar.
- Type
React.ReactNode
loading- Description
- Swap the leading glyph for a spinner and set `aria-busy`.
- Type
boolean- Default
false
maxWidth- Description
- Max label width when `truncate` is set (px number, or any CSS length).
- Type
number | string- Default
200
progress- Description
- A completeness ring in the leading slot; 0–100, snaps to a check at 100.
- Type
number
pulse- Description
- Add a reduced-motion-safe pulsing ring around the `dot`.
- Type
boolean- Default
false
trailing- Description
- Fixed-width content after the label — an icon, count, or the copy button.
- Type
React.ReactNode
truncate- Description
- Ellipsize a long label at `maxWidth`, exposing the full text via `title`.
- Type
boolean- Default
false
Plus 280 inherited native <span> attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="badge"][data-slot="badge-leading"][data-slot="badge-label"][data-slot="badge-trailing"][data-slot="badge-separator"][data-slot="badge-dot"][data-slot="badge-copy"][data-slot="badge-count"]States
| Selector | State | Description |
|---|---|---|
:focus-visible | focus-visible | Ring on the asChild anchor / the copy button (the only focusable parts). |
| loading | Spinner replaces the leading glyph; sets aria-busy. |
| truncated | Label ellipsizes at maxWidth; full text exposed via title. |
| Property | Token | Tier |
|---|---|---|
fill | --garn-badge-success-bgbrandable | semantic |
ring | --garn-ringbrandable | semantic |
Private instance vars (never externalize): --b-soft-bg--b-soft-fg--b-solid-bg--b-solid-fg--b-line--b-text--badge-c--badge-fg
When to use
Tag or categorize inline — a status, count, or short label next to content.
Reach for something else when
- An interactive control (use a button or toggle)
- a removable or selectable chip, or a value-changing dropdown — those belong to Tag / SelectBadge
- large notification counts belong to Counter.
Overview
Badge tags or categorizes inline — a status, a short label, a count beside content. Its look is orthogonal: tone sets the meaning color (neutral / brand / success / warning / danger / info), appearance sets the fill (solid / soft / surface / outline), and size the scale — the same vocabulary across the whole chip family. It's a read-only marker; for a removable/selectable chip use Tag, for a value the user changes use BadgeSelect, and for a pure number use Counter.
Guidelines
Let tone carry meaning, consistently. Map each status to one tone and reuse it everywhere (success = done, danger = error); a rainbow of tones for decoration trains people to ignore them. Keep the label to a word or two — a badge is a marker, not a sentence.
Don't make a badge do a control's job. It isn't clickable, focusable, or dismissible by contract; if users need to remove or toggle it, reach for Tag or BadgeSelect. Place it adjacent to what it describes so the association is obvious.
Mind contrast and redundancy. Pair tone with text (or an icon) so meaning survives for color-blind users and in forced-colors mode. Use soft/surface for quiet metadata and reserve solid for the one status that must stand out.
Best practices
- Keep the text to one or two words.
- Use a tone for state (success/warning/danger) with a readable label — not color alone.
- Reach for
framedwhen a soft chip needs more definition;solidfor the highest emphasis.
- Don't make a badge clickable — its only interaction is optional copy-to-clipboard.
- Don't pack sentences into a badge; only the label truncates — keep leading/trailing to glyphs.
Content guidelines
- Keep the label to one or two words — a state or category, not a sentence (“Paid”, “In review”).
- Use sentence case, and prefer a noun or adjective over a verb (“Active”, not “Activated it”).
- Pair tone with text, never color alone — a success badge still reads “Paid”, not just green.
- Keep the same label for the same state across the product so it stays scannable.
- For counts, show the number only (“12”), and let Counter handle large or notification-style totals.
Troubleshooting
A custom-colored solid badge has unreadable (assumed-white) text.
Cause. getReadableForeground parses hex/rgb; design tokens / oklch can't be parsed synchronously and fall back to white.
Fix. Pass color as a hex/rgb value for auto-contrast, or set an explicit foreground.
Editing dark-mode badge colors in the component has no effect.
Cause. Tones resolve through the --garn-badge-* component tokens; the dark values live in the token layer.
Fix. Adjust dark mode in the garn theme CSS (.dark block) — never with raw ramps in badge.tsx.
An icon-only badge isn't announced by screen readers.
Cause. No text label and no aria-label (the dev build warns).
Fix. Pass aria-label (or aria-labelledby / title / sr-only text).
A Badge.Separator with variant="line" shows nothing where you passed content.
Cause. The line variant is a pure hairline and discards children by design; only variant="dot" renders text (the middot, or your override).
Fix. Use the default dot variant when you need a visible glyph/character; keep line for a plain divider between labels.
Accessibility
- Focus
- 2px ring in --garn-ring on the asChild anchor and the copy button.
Accessibility requirements
Provide aria-label (or aria-labelledby / title / sr-only text).
when no text label (icon/dot/avatar only) && !asChild && no aria-label/labelledby/title
- Badge is a plain <span> with no implicit role — keep a readable text label; don't encode status by color alone.
- loading sets aria-busy. copyable renders a real <button> with an aria-label and announces via an aria-live=polite region.
- Badge.Separator is aria-hidden and select-none — purely decorative, so it never enters the badge's accessible name. A composed action (e.g. an inline link) keeps its own name and focus ring; Badge itself stays non-interactive.