Components

Badge

The small status chip — a read-only label that carries a record's state: a row's status, a finding's severity, a flag in a detail hero. Officially it's a tinted chip — a soft fill of the tone, deep tone text, and a hairline border, with an optional status dot or severity icon. Easy to spot, easy to scan.

Five tones map straight onto the semantic ramp — neutral · info · affirm · caution · critical. The text label is always the signal, so color is reinforcement, not the message: a badge reads correctly in monochrome and for color-blind users (ADR-0017). Dark mode swaps the soft tints for dark surface tints, so the chip stays legible without changing its meaning.

Tones

Neutral is a plain gray chip — no severity, just a label. Info is the silver-blue of a state worth noting. Affirm is green, caution is bronze, critical is red — the same status colors the rest of the product speaks. Pick the tone for the meaning, never the color for the look.

Draft
tone: neutral
Gray. A plain label with no severity — a lifecycle stage like a draft.
Submitted
tone: info
Silver-blue. A neutral-but-notable state.
Active
tone: affirm
Green. A good / active / passing state.
In review
tone: caution
Bronze. Needs attention — review, pending, expiring.
Flagged
tone: critical
Red. A failing / blocked / flagged state.

Dot and icon

For severity tones, pair a non-colour cue so the meaning survives a greyscale print or a colour-blind reader (ADR-0017). A dot is the low-key nudge; a Lucide icon says it outright — the right call for caution and critical. The icon wins when both are set.

ActiveIn reviewFlagged
dot
A tone dot — a small, wordless backup for the label.
ActiveIn reviewFlagged
icon
A Lucide icon, inheriting the tone. Explicit; best for severity.

Solid

The one loud option — a chip filled with the full tone color and white type, the same recipe as a filled button. The fill is deep enough that contrast holds in both themes. Save it for the single status that has to dominate (a critical flag in a detail hero), not for table cells, where a whole column of them gets noisy fast.

DraftSubmittedActiveIn reviewFlaggedFlagged

In a table

A status column is where badges live — tinted chips with a dot make a friendly, scannable column. See it live in the /data-table status column.

Materiality

Everything resolves to tokens; the badge owns no raw values. No glass, no engraving — just a flat tinted chip that gets out of the way.

tinted chip
A soft fill of the tone with a hairline border in the same tint. The proposed system goes frameless here; the official badge is a friendly little chip instead.
tone text
The label paints from the tone ramp at the -700 step — deep color on the light tint, so it reads without squinting. Neutral rides the theme-aware --fg-2 instead.
flat text
Engraving belongs to the proposed system — --engraving-shadow has no job here. Official badge text is flat: solid color, no shadow, nothing to catch the light.
solid fill
The solid emphasis fills with the -700 step and sets fixed near-white --ink-25 type on top — clean white on color, like a filled button, in both themes.

Props

tone?: BadgeTone
'neutral' | 'info' | 'affirm' | 'caution' | 'critical'. Defaults to 'neutral'.
emphasis?: BadgeEmphasis
'plain' (default, tinted chip) | 'solid' (filled, white type). Reach for emphasis="solid" only when one status must dominate.
dot?: boolean
Renders a leading tone dot — a non-colour reinforcement for severity (ADR-0017).
icon?: ReactNode
A leading Lucide icon, inheriting the tone. Recommended for severity tones; takes precedence over dot.
...HTMLAttributes
Renders a <span>; remaining span attributes (className, title, data-*) pass through. Read-only by design — a badge is not interactive.

Usage

A status badgetsx
import { Badge } from '@halo-compliance/ui'

<Badge tone="affirm" dot>Active</Badge>
Severity with a Lucide icontsx
import { Badge } from '@halo-compliance/ui'
import { TriangleAlert } from 'lucide-react'

<Badge tone="critical" icon={<TriangleAlert strokeWidth={1.75} />}>
  Flagged
</Badge>
Mapping a status to a tone in a table celltsx
const STATUS_TONE: Record<Status, BadgeTone> = {
  Active: 'affirm',
  Review: 'caution',
  Inactive: 'neutral',
}

cell: ({ getValue }) => {
  const status = getValue() as Status
  return <Badge tone={STATUS_TONE[status]} dot>{status}</Badge>
}

When to use

  • A record's state. A status, a stage, a severity, a flag — anything you'd read off a row or a hero.
  • Reinforced, not signalled, by colour. The label is the meaning; the tone (and dot / icon) reinforce it.
  • Read-only. If it does something when clicked, it is a Button or a filter chip, not a badge.

Anti-patterns

  • Colour as the only signal. A bare red dot with no label fails ADR-0017 — give it a word.
  • Solid everywhere. A column of filled chips gets loud; tinted chips with a dot stay easy to scan.
  • A clickable badge. If it removes a filter or opens a menu, that is a chip or a button — keep the badge inert.
  • Tone for decoration. Pick the tone for the meaning; brand gold is for buttons, not statuses.
  • /data-table — the data grid whose status column these fill.
  • /text-groups — the mono nameplate; the badge is its small status sibling.
  • /helper-text — the severity message register the tones share (ADR-0017 icon pairing).
  • /colors — the semantic ramp (--affirm-700, etc.) the tones resolve to.