The house vocabulary for how a record is doing, in two sizes. StatusPill is the full label for a row, job, or record's discrete state; StatusDot is the compact sibling for tight spaces.
The canon behind it. docs/design-language/03-status-language.md §A1 (03-status-language.md:13, three small-tag species, never interchangeable), §A4 (03-status-language.md:22, a pending mutation is not a status), §A5 (03-status-language.md:25, vocabulary is fixed, small, and shared), and §A8 (03-status-language.md:36, identity colors mark kind, not condition).
When to reach for it#
Reach for StatusPill/StatusDot for STATE: how a thing is doing right now (queued, running, done, failed, needs review). Both share StatusKind (ok | warn | err | info | review | neutral), drawn from the theme's status color pairs.
Do not reach for either to encode SELECTION or NAMING or IDENTITY; the canon calls these four things out as separate vocabularies that are never interchangeable (03-status-language.md §A1). Chip communicates selection (a filter, a removable tag). FieldChip communicates naming (a schema field or a source file). Identity color, a module's or a chain's own color, is informational and never means "healthy" or "unhealthy": Speedway's ModuleBadge and ChainBadge are separate app-local components (speedway/app/components/ModuleBadge.tsx), not built from StatusPill, precisely because painting a module's identity in status colors would make a reader ask what's wrong with it. A pending mutation is not a status either: "a pending cancel is NOT a badge state, the action button carries it as a loading label" (03-status-language.md §A4); see button.md for where that state actually lives.
StatusDot in this kit is the smaller, plainer sibling: kind plus an optional label and pulse, meant for ledgers, tab bars, and steppers. It has exactly one shipped call site in either app (walmart-mvp/frontend/src/pages/Home.tsx:363), without label or pulse. Do not confuse it with walmart-mvp/frontend/src/features/catalog/pipeline.tsx:169's own StatusDot, a same-named local component with a completely different signature (status/accented, not kind/label/pulse) used across Workspace.tsx and CatalogDetail.tsx for the catalog pipeline's five-stage tracker. The two are not interchangeable and importing the wrong one is a real risk: walmart imports the kit's version from @versable-git/ui in some files and its own from ./pipeline in others, by name alone.
Contract#
StatusPill:
kind: StatusKind(required).size:sm | md, defaultmd.variant:soft | line | solid | soft-outline, defaultsoft. Onlysoftandsoft-outlinehave shipped usage in either app;lineandsolidare implemented (status-pill.tsx:17-55, solid inverts the soft/line pair rather than adding a third tone) but no shipped call site reaches for either.dotrenders a small leading dot in the kind's full-strength color;dotPulsepulses it, the "actively working" cue for an in-progress stage. Both have real shipped usage:dotPulsealone on aneutral/infopill for an ingesting or running job stage (speedway/app/components/JobsTable.tsx:150-168), anddot dotPulse={active}together on a stage-progress pill whereactiveis computed from the job's status (walmart-mvp/frontend/src/pages/Jobs.tsx:118-131).countrenders a number after the label in tabular nums at reduced opacity. Shipped usage drives an interactive filter toggle, the pill's own count reflecting a live filtered length (walmart-mvp/frontend/src/pages/Jobs.tsx:842-848).Iconand the rest ofIconImportProps(iconSize,iconClassName) render a leading icon before the label.iconPropsis part of the type (StatusPillProps extends IconImportProps) butstatus-pill.tsxnever destructures or forwards it, so passing it today is a silent no-op; seeicon.md.skeletonrenders a pill-shaped loading placeholder (a rounded bar sized tosize) instead of content. No shipped usage found in either app; both apps skeleton the row or cell a status pill sits in rather than the pill alone.
CodePill, the pill for a module's own codes:
code: string(required) andcodes: Record<code, { label, description?, kind? }>(required): the map a module's manifest declares for itserror_typesandreview_reasons(gcp/contract/contracts/manifest.mdrows 42 and 43: the label is the short human text a pill shows, the description the one-line tooltip, "so an app never invents strings for module codes"). The entry'slabelis the pill and itsdescriptionthe tooltip.kind?: StatusKind: the tone for every code in the map that does not name its own (errfor error types,reviewfor review reasons are the usual calls); an entry's ownkindwins;neutralwhen neither says.- A code the map does not declare renders as typed, in mono and the neutral tone, with a "Not a code this module declares" tooltip. A stale map shows rather than being papered over with an invented label. Every other
StatusPillprop (size,variant,dot,Icon,count) passes through.
StatusDot:
kind: StatusKind(required).labelrenders text beside the dot;size:xs | sm, defaultsm.pulsedraws an expanding-ring animation behind the dot for an in-progress stage.- No
skeletonprop exists onStatusDot, unlikeStatusPill.
Both pull from the same six-member StatusKind union so that pills, dots, and any future status-driven surface stay visually consistent rather than each inventing its own vocabulary (status.types.ts:5-9); neutral is the no-status baseline, not tied to a dedicated soft/line color pair the way the other five kinds are.
Sanctioned combinations#
| Combination | Produces | Where used | Why |
|---|---|---|---|
variant="soft-outline" dot dotPulse={active} where active is derived from the record's own status field | A stage-progress pill whose pulse state is a direct function of data, not a separate flag the caller tracks | walmart-mvp/frontend/src/pages/Jobs.tsx:118-131, speedway/app/components/JobsTable.tsx:143-172 | Canon preset: "soft-outline + leading dot (+ dotPulse when active), roomier px" (03-status-language.md §B) |
kind computed through a small lookup map keyed by the domain's own status enum, shared between the table cell and any detail view of the same record | One status-to-kind mapping reused everywhere that record's status renders, instead of re-deriving it per call site | JobsTable.tsx:141-143 (comment: "Label text is shared with the job page... so a status never reads two ways"), walmart-mvp/frontend/src/pages/Jobs.tsx:344 (STAGE_LOG_LEVEL_KIND[l.level]) | Canon §A5: "vocabulary is fixed, small, and shared" |
count bound to a live filtered array length, wrapped in a clickable element that toggles the filter | A status pill doing double duty as a facet toggle with its own live count | walmart-mvp/frontend/src/pages/Jobs.tsx:836-849 | The pill already carries the kind and the label the filter represents; count keeps the affected row total visible without a second element |
kind="neutral" for a benign non-color state (queued, cancelled) rather than reserving neutral for "no status" | A visually quiet pill for a state that is real but not urgent | JobsTable.tsx:208 (kind="neutral" for queued) | neutral has no dedicated soft/line pair and borrows base chrome, which reads as calm rather than alarmed; matches canon's queued/cancelled treatment (03-status-language.md §C) |
Banned combinations#
Do not use StatusPill/StatusDot for a pending mutation (a cancel in flight, a save in progress). The canon is explicit that this belongs on the acting button as a loading label, never on the record's status badge (03-status-language.md §A4); see button.md.
Do not use StatusPill where the canon calls for a Chip or FieldChip instead: selecting/filtering values, or naming a field or file. Reaching for StatusPill because it is the nearest small-tag component available produces a pill that visually claims to describe state when it is actually describing selection or naming.
Do not import walmart-mvp's local pipeline.tsx StatusDot expecting the kit's kind/label/pulse props, or vice versa. The two share a name and nothing else; check the import source (@versable-git/ui versus ./pipeline or @/features/catalog/pipeline) before reading or writing a call site that uses StatusDot.
Do not paint an identity concept (a module, a chain, a data source) in StatusKind colors. Identity color marks kind, not condition; the canon states ink for identity never signals health (03-status-language.md §A8). Reach for an app-local identity component instead, as Speedway's ModuleBadge/ChainBadge do.
Before you adopt this#
Five questions to answer before reaching for StatusPill/StatusDot.
- Does the shell, a parent layout, or a global provider already render this? Not applicable, StatusPill/StatusDot are inline content; no shell already renders them.
- Does this app already ship a local implementation of the same thing? Walmart's
pipeline.tsxships its own unrelatedStatusDot; check the import source. - Does this app's kit pin reach the version this component or prop landed in? Not applicable, no version-landed StatusPill/StatusDot prop is noted in this doc.
- Does the component derive its own accessible name and keyboard path, or must the call site supply them? Not applicable, these are non-interactive tags with no keyboard path to supply.
- Which canon §A rules bind this surface, and which does the composition break? §A4 keeps a pending mutation off the badge; §A8 keeps identity colour off condition.
Travels with#
RenderIcon, for StatusPill's leading Icon; see icon.md.
A per-domain kind lookup map (e.g. JOB_STATUS_KIND, STAGE_LOG_LEVEL_KIND, PART_STATUS_KIND), local to the app, translating that domain's own status enum into StatusKind. The canon frames third-party or domain vocabularies as translated at the boundary through one explicit switch (03-status-language.md §A5); grep for _KIND or _STATUS_KIND in a page before writing a new one.
Tooltip, wrapping a StatusPill used as an interactive filter toggle, to name what clicking it does (Jobs.tsx:836-841).
Snippet#
// walmart-mvp/frontend/src/pages/Jobs.tsx:118-131 (trimmed)<StatusPill kind={JOB_STATUS_KIND[r.status]} size="sm" variant="soft-outline" dot dotPulse={active} className={onStageClick ? "transition-colors group-hover:border-current" : undefined}> {PIPELINE_STAGE_LABELS[i]} ({i + 1}/{PIPELINE_STAGE_LABELS.length})</StatusPill>