Skip to content

Badge

A static, inline label for a status, count, or short piece of metadata.

Stablev0.4.2added in v0.1.0@garn/ui/badge
On 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.

terminal
npx garn-ui add badge
First time? Set up garn in your project
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

SelectorState
:focus-visiblefocus-visible
loading
truncated
PropertyTokenTier
fill--garn-badge-success-bgbrandablesemantic
ring--garn-ringbrandablesemantic

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

Do
  • 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 framed when a soft chip needs more definition; solid for the highest emphasis.
Don't
  • 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

warn

Provide aria-label (or aria-labelledby / title / sr-only text).

when no text label (icon/dot/avatar only) && !asChild && no aria-label/labelledby/title

A1.4.1Use of ColorAA1.4.3Contrast (Minimum)AA2.4.7Focus Visible
  • 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.