sc1m/design
ComponentsBadge

Badge

Badge: a status chip carried by fill alone — a tint of the tone at 12–15%, label inked toward the hue, and a sharp radius from --brand-radius-sm. Five tones (neutral, info, success, warning, danger) plus an optional icon slot. No runtime dependencies.

Stable

Live preview

PaidPendingRetryingOverdueDraft

Installation

pnpm dlx shadcn@latest add @sc1m/badge

This also merges the token layer and installs src/lib/cn.ts. For the manual route — prerequisites, components.json, and app-level setup — see Installation.

Usage

import { Badge } from "@/components/ui/badge";

Composition

One element. tone picks the fill, icon fills the optional leading slot.

Badge├── (icon — optional, aria-hidden)└── children

Tones

Four tinted tones plus a neutral. Neutral is not a 0% tint of some arbitrary hue — it is the absence of a state, so it takes the plain surface.

PaidPendingRetryingOverdueDraft
<Badge tone="success">Paid</Badge>
<Badge tone="info">Pending</Badge>
<Badge tone="warning">Retrying</Badge>
<Badge tone="danger">Overdue</Badge>
<Badge>Draft</Badge>

Icon

The study is deliberately glyphless — the fill carries the state on its own, and every glyph you add is ink the tint had already earned. Reach for the slot when a badge has to survive greyscale, or sits somewhere the tint alone is ambiguous.

Status glyphs take Phosphor's fill weight — see Icons.

The slot fixes the box and stops the glyph shrinking, so a 16px or 24px source icon still lands at the label's optical weight. It inherits the tone's ink through currentColor, and is aria-hidden — the label is what gets read.

PaidOverdue
import { CheckCircle } from "@phosphor-icons/react/dist/ssr";

<Badge tone="success" icon={<CheckCircle weight="fill" />}>
  Paid
</Badge>

Example

The preview above, with its source. This is the demo module verbatim, so the code and the thing it renders cannot drift apart.

PaidPendingRetryingOverdueDraft
<div className="flex flex-wrap gap-2">
  <Badge tone="success">Paid</Badge>
  <Badge tone="info">Pending</Badge>
  <Badge tone="warning">Retrying</Badge>
  <Badge tone="danger">Overdue</Badge>
  <Badge>Draft</Badge>
</div>

API reference

Badge forwards every prop of a span.

PropTypeDefaultDescription
tone"neutral" | "info" | "success" | "warning" | "danger""neutral"Fill and label ink. Neutral takes the plain surface.
iconReactNodeOptional leading glyph. Sized by the slot, hidden from assistive tech.

Accessibility

  • A badge is a label, not a live region. It announces as its text and takes no role of its own — if a badge reflects state that changes while the page is open, put the live region on the container that owns that state.
  • The icon slot is aria-hidden, so the label carries the whole meaning. Never ship an icon-only badge: the tint is not readable in greyscale, and the glyph is not readable at all.
  • Colour is never the sole carrier of meaning here — the label always says what the tint says.

Design tokens

The component reads semantic tokens only — no raw colour, radius, or duration appears in its source. Override the token, not the component.

TokenApplied to
--infoinfo fill and label ink
--successsuccess fill and label ink
--warningwarning fill and label ink
--dangerdanger fill and label ink
--surfaceneutral fill
--fg-mutedneutral label
--bgwhat each tone is washed into
--fgwhat each label ink is pulled from
--brand-radius-smcorner radius — 2px in sc1m

Source

src/components/ui/badge/badge.tsx — no next/* imports, so it drops into any React 19 + Tailwind v4 app.