Resolved: light

Badges, pills & chips

The small-status family — compact labels that annotate state, category or counts. Four React primitives (Badge, StatusPill, NotificationBadge, IconChip) plus the raw .chip helper classes. All draw from the same theme-aware semantic palette (good / warn / bad / accent + the surface ramp), so flip the theme in the top bar to confirm every tone re-skins. None of them ever rely on color alone: every example carries a word or an icon that names its meaning.

<Badge>
Tiny static label (cva: tone × size). For a category, attribute or fact next to content — “Verified”, “Beta”, “3 dogs”.
<StatusPill>
Status-forward pill — larger, rounded-full, with an optional leading dot and a motion-safe pulse for live/in-progress states.
<NotificationBadge>
A count chip for unread/pending. Renders null at 0, clamps to “max+”, exposes an aria-label.
<IconChip>
The square rounded icon container used at the head of ActionCard / ListRow, with tones, sizes and a locked overlay.
.chip / .chip-sm
Raw CSS helper classes (globals.css) for risk/team/role tags rendered outside React — pair the base with a tone modifier.
tsx
import { Badge, StatusPill } from "@/components/ui";

<Badge tone="green">Verified</Badge>
<StatusPill tone="amber" size="md">Pending</StatusPill>

Badge — tone × size matrix

Every tone crossed with every size. Rows are tones (gray · red · green · amber · blue · accent), columns are sizes (sm · md). gray is the default tone, sm the default size; size changes padding and font size only, never the color.

tone \ sizesmmd
graygraygray
redredred
greengreengreen
amberamberamber
blueblueblue
accentaccentaccent
tone (rows) × size (columns)
size="sm"
px-2 py-0.5 text-[11px] — the default (defaultVariants.size). Inline within dense rows / table cells.
size="md"
px-3 py-1 text-sm — roomier; standalone or next to body text.

Badge — tones & meaning

The six tone keys and what each one means. Reach for the semantic tones (red / green / amber) only when the badge actually carries that state; use gray for neutral facts and blue / accent for brand-flavoured information.

DraftVerifiedPendingOverdueBetaFeatured
all six tones at size md, with real labels
tone="gray"
bg-surface-3 / text-muted — the neutral default (defaultVariants.tone). Categories, counts, anything non-semantic.
tone="red"
bg-[--color-bad-bg] / text-bad — error, blocked, overdue. Red carries meaning, never decoration.
tone="green"
bg-[--color-good-bg] / text-good — success, verified, active, complete.
tone="amber"
bg-[--color-warn-bg] / text-warn — warning, pending, needs attention.
tone="blue"
bg-accent-soft / text-accent — informational, in the brand (beryl) family. Same palette as accent.
tone="accent"
bg-accent-soft / text-accent — brand emphasis. Identical styling to blue today; kept as a distinct semantic key.

StatusPill — tone × size matrix

The same six tones as Badge, but on the larger, rounded-full pill with three sizes (sm · md · lg; md is the default). Each cell carries a status word so the meaning never rides on color. StatusPill is for state that changes — Badge is for static facts.

tone \ sizesmmdlg
grayDraftDraftDraft
redBlockedBlockedBlocked
greenActiveActiveActive
amberPendingPendingPending
blueIn reviewIn reviewIn review
accentFeaturedFeaturedFeatured
tone (rows) × size (columns)
size="sm"
px-2.5 py-1 text-xs, dot h-1.5 w-1.5.
size="md"
px-3 py-1.5 text-sm, dot h-2 w-2 — the default (defaultVariants.size).
size="lg"
px-4 py-2 text-base, dot h-2.5 w-2.5 — hero / header status.

StatusPill — dot & pulse

Two booleans add a leading status dot. dot shows a solid dot tinted to the tone (currentColor); pulse adds an animated ping ring for live / in-progress states and implies dot. The pulse is gated behind motion-safe, so it goes still under prefers-reduced-motion.

Active(no props)
Activedot
Livepulse
plain · dot · pulse — same tone, three treatments
DraftBlockedActivePendingIn reviewFeatured
dot across every tone (size md)
SyncingIn progressAwaiting review
pulse — typical live states (sm · md · lg)
dot
boolean (default false). Adds a solid leading dot tinted to the tone via bg-current. Dot size tracks the pill size (sm/md/lg → 1.5/2/2.5).
pulse
boolean (default false). Adds a motion-safe animate-ping ring behind the dot for live/in-progress states. Implies dot — you don’t need both.
reduced motion
The ping is wrapped in motion-safe:, so under prefers-reduced-motion the dot is shown without animation.
aria-hidden dot
The dot/pulse element is aria-hidden — meaning is carried by the pill’s text label, never the animation.

NotificationBadge — counts

A round count chip for unread / pending items. It renders nothing at 0 (or below), so you can pass a raw count unconditionally, and clamps anything over max (default 99) to “max+”. tabular-nums keeps the width steady as the number changes.

count=0
count=1
count=9
count=42
count=99
count=100
0 (renders null) · 1 · 9 · 42 · 99 · 100 (→ 99+)
max=9
custom max — count={250} max={9} → 9+
anchored to an icon (overlap pattern)
count
number (required). Renders null when <= 0. Pass it raw — no need to conditionally hide the badge yourself.
max
number (default 99). Counts above this display as `${max}+` (e.g. 100 → 99+).
label
Optional (count) => string for the aria-label, receiving the raw unclamped count. Default: `${count} new`.
role=status
The badge is a live region with an aria-label; the visible text is aria-hidden so AT reads the friendlier label, not “99+”.

NotificationBadge — tones

Three tones. red is the default (urgent / unread); accent is the brand-toned alternative; gray is the quiet, low-priority option. The clamp and null-at-zero behaviour is identical across all three.

tone="red"
tone="accent"
tone="gray"
all three tones at the same count
tone="red"
bg-bad / text-accent-foreground — the default. Unread / urgent counts.
tone="accent"
bg-accent / text-accent-foreground — brand-toned counts.
tone="gray"
bg-surface-3 / text-muted — quiet, low-priority counts.

IconChip — tone × size matrix

The square, rounded icon container that heads an ActionCard or ListRow. Three tones (indigo · gray · green; indigo is the default) crossed with three sizes (sm · md · lg). The box rounds harder as it grows (md → rounded-md, lg → rounded-xl); pick a glyph size that fills the box (h-4/h-5/h-6).

tone \ sizesmmdlg
indigo
gray
green
tone (rows) × size (columns)
size="sm"
h-7 w-7 rounded-md — compact list rows.
size="md"
h-9 w-9 rounded-lg — the default (size prop default).
size="lg"
h-11 w-11 rounded-xl — card headers / hero rows.
children
Typically a lucide icon already sized to the chip. Decorative — mark it aria-hidden; name the chip’s context in the surrounding row.

IconChip — tones & locked state

The three tones with real glyphs, plus the locked state. locked dims the icon (opacity-60 grayscale) and overlays a small lock badge bottom-right — use it to mark a tool or action the viewer can’t yet access. When locked, the glyph is hidden from assistive tech (the lock carries the meaning), so label the lock’s reason in the surrounding row.

indigo
gray
green
tones — indigo (default) · gray · green
size="sm" — open · locked
size="md" — open · locked
size="lg" — open · locked
locked — unlocked vs locked, every size
tone="indigo"
bg-accent-soft / text-accent — the default (brand beryl). The usual tool/section chip.
tone="gray"
bg-surface-3 / text-muted — neutral / inactive.
tone="green"
bg-[--color-good-bg] / text-good — positive / completed.
locked
boolean (default false). Dims the icon (opacity-60 grayscale) and adds a bottom-right lock badge. The glyph becomes aria-hidden — surface the locked reason in the row/card text.

Raw .chip / .chip-sm helper classes

CSS-only helper classes (src/app/globals.css) for risk / team / role tags rendered outside the React layer — e.g. server-rendered roster tables. Pair the base .chip (or the denser .chip-sm) with one tone modifier. Unlike Badge/StatusPill these are pill-shaped (radius-pill) and carry a tinted border. The risk tones (red/yellow/green) are deliberately semantic — they encode risk level, not decoration.

neutralredyellowgreenhuskybulldogterrierleaddirectoradvise-against
.chip — base size, every tone modifier
neutralredyellowgreenhuskybulldogterrierleaddirectoradvise-against
.chip.chip-sm — the denser variant, every tone
.chip
Base class: inline-flex pill, padding 2px 10px, 12px / weight 540, radius-pill, 1px transparent border. Needs a tone modifier for color.
.chip-sm
Denser modifier: padding 1px 8px, 11px text. Combine as `chip chip-sm chip-<tone>`.
.chip-neutral
Neutral — surface-3 fill, muted text, border edge.
.chip-red
Risk: red (semantic — danger/high).
.chip-yellow
Risk: amber (semantic — caution/medium).
.chip-green
Risk: green (semantic — safe/low).
.chip-husky
Team: husky → accent (beryl).
.chip-bulldog
Team: bulldog → red.
.chip-terrier
Team: terrier → green.
.chip-lead
Role: lead → accent.
.chip-director
Role: director → accent-strong.
.chip-advise-against
Flag: advise-against → warn/amber.

When to use which

Four primitives plus the raw helpers overlap visually — here’s how to choose. The rule of thumb: Badge for a static fact, StatusPill for a changing state, NotificationBadge for a count, IconChip for a section/tool icon, and the .chip classes only when you can’t reach for a React component.

VerifiedBadge
LiveStatusPill
NotificationBadge
IconChip
the four React primitives side by side
Badge
A static attribute or fact attached to content — role, category, “Beta”, a small count label. It doesn’t imply the thing is changing.
StatusPill
A lifecycle state that can change — Active / Pending / Blocked / Live. Reach for it (not Badge) when you want the dot or the live pulse.
NotificationBadge
A pure numeric count of unread / pending items, usually anchored to an icon or nav item. Hides itself at 0.
IconChip
Not a label at all — the square icon container at the head of an ActionCard / ListRow, with the locked overlay for gated tools.
.chip / .chip-sm
Last resort: risk/team/role tags in CSS-only / server-rendered markup where importing a React component isn’t practical.
never color alone
Across all of them, meaning rides on a word or icon — the hue only reinforces it (needed for color-blind users and AT).