Badges, chips, pills, dots, banners, alerts, and the vocabulary rules behind them. Kit sources: packages/ui/src/status, chip, field-chip, alert. Evidence registers under docs/evidence/20260807-0230-speedway-design-language/ in this repo.
Audience: an agent (or person) choosing badges, chips, pills, or banners.
§A Principles and mental models#
1. Three small-tag species, never interchangeable.
StatusPill/StatusDot communicate STATE. Chip communicates SELECTION (always a real button, demoted to span-with-close when removable, for valid HTML). FieldChip communicates NAMING (a schema field or file). The file tone is warm amber because "a file is a thing you can open". Misusing one for another's job is a defect.
2. Six status kinds, two visual weights.
StatusKind = ok | warn | err | info | review | neutral, drawn from theme soft/line contrast pairs. StatusDot for tight spaces (tab triggers, ledgers, steppers), with pulse as the "actively working" ring. StatusPill for standalone display: sizes sm/md, variants soft | line | solid | soft-outline. Solid inverts the soft/line pair rather than minting a third tone. Deliberately not daisyUI badge ("padding reads as bloated; a pill is meant to read quiet").
3. Status consolidates. It never dumps raw signals. The house pattern is a ranked collapse of many raw signals into ONE badge with a stated priority order: ModuleStatus ("review beats everything, then a run in flight, then output landed, then finished-empty"), StagePill (error → ingesting → running → setupHold → flagged → done → queued). setupHold is styled as a to-do (primary tint), "held jobs are a to-do, not a wait state."
4. A pending mutation is not a status.
"A pending cancel is NOT a badge state, the action button carries it as a loading label" (JobStateBadge.tsx:36-39). Badges describe the record. Buttons describe the in-flight intent.
5. Vocabulary is fixed, small, and shared.
- Review reasons: exactly three surfaced words, Missing / Invalid value / Conflict, one shared map across row chip, editor, and filter. Content items always surface as "Missing" whatever the internal reason (owner 2026-07-30, stated at every callsite). Severity is two-tier under the three labels: Missing = warn, Invalid/Conflict = err.
- Decision history: Saved (ok), Edited (info), Dismissed (neutral), Reverted (warn), one map reused between table and history panel.
- Third-party vocabularies are translated at the boundary into house kinds via one explicit switch (scrape-requests: delivered→ok, error→err, cancelled→neutral, everything in flight→warn).
6. Non-trivial status explains itself.
Every rich badge carries a tip stating why ("A module left a value for a person to settle"), and count-bearing pills use tabular nums. A suggestion's presence is its own layered cue: a slow pulsing dot (2.8s, deliberately calmer than default) beside the reason pill, tooltipped with the suggested value.
7. Banners: tone separates error from note. The reason precedes what it explains.
IngestOutcomeBanner paints rejected files error-tinted and no-data-rows files neutral, "the banner must not paint the two the same colour." Banners render ABOVE the tables they explain: "'Failed' and '0 parts' are both unreadable without it." Kit Alert (info/success/warning/error/neutral) is the inline callout. Toasts are for transient events. A successful action may still carry a non-blocking warning string.
8. Identity colors mark kind, not condition. ModuleBadge (quiet ring chip + identity dot) is purely informational. ChainBadge wears the same look but links, with stopPropagation inside clickable rows. Identity ink never signals health.
§B Presets#
Each row names the situation, the composition that reaches it, and the showcase example that renders it, so this table is the layer between a principle above and a contract doc below.
| Situation | Preset | Reaches it, and where it renders |
|---|---|---|
| Table status cell | col.status factory → StatusPill from a row mapper | col.status(mapper) in the column list; the data-table gallery (apps/playground/src/app/components/data-table/gallery.tsx:174) |
| Stage/state pill | soft-outline + leading dot (+ dotPulse when active), roomier px | StatusPill with dot and dotPulse={active}; the status gallery's live pill row (apps/playground/src/app/components/status/gallery.tsx:47) |
| Tight status | StatusDot sm, pulse for in-flight | StatusDot kind size="sm" pulse; the compact dot table (apps/playground/src/app/components/status/examples/status-dot.tsx:19) |
| Needs-review count | amber bold count or warn pill; green 0 when clear | a known-only count on a rail item, nothing until the number is real (apps/playground/src/app/patterns/sidebar-anatomy/example.tsx:31) |
| Error vs note banner | Card with error/25 + error/5 tint vs base-300 + base-200/40 | no showcase example yet |
| Suspended / hard states | Alert tone="error", customer copy plain and action-oriented; staff copy technical | Alert tone="error" with a plain string child; the alert gallery (apps/playground/src/app/components/alert/gallery.tsx:19) |
| One-time secret fallback | Alert warning + readonly Input with select-all-on-focus | no showcase example yet |
| Member state beside a role control | StatusPill info sm for a pending invite; the row's own colour untouched | StatusPill kind="info" size="sm" next to the role Select (apps/playground/src/app/patterns/member-row-inline-role/example.tsx:103) |
| Event tone in a feed | StatusDot per event on a connector line, grouped by day | one StatusDot per row, tone from the event kind (apps/playground/src/app/patterns/recent-activity-feed/example.tsx:68) |
§C Use cases through Speedway#
- JobStateBadge: queued/Running share warn kind, distinguished by icon + label only, with done=ok, cancelled=neutral, error=Failed/err.
- Review reason chips + the has-suggestion pulse dot. Log's decision pills.
- Content fields: unfilled renders "Dismissed" (user) or "Missing" (anything else), the review queue's word by ruling.
- Nav badges: plain numeric spans. Amber only when attention is owed. No badge at all when the count is not loaded.
- Auth: deliberately uninformative status ("Incorrect email or password", unconditional "If an account exists..." with constant-time verification), a security constraint on copy, not a tone choice.
- Known deviations to keep bounded: loadsheet readiness uses raw daisyUI badges. Contentgen preview uses raw badge-warning/error cells.