StatusPill
A short label that says which of four levels of attention something needs. It is shown as a word, an icon and a colour together, never as a colour alone.
Preview
status-pill · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/status-pill
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { StatusPill } from "@/components/ui/status-pill"<StatusPill status="attention" describes="Second example measurement" />When to use it
Use it when
- Labelling one result, reading or item with the level of attention the product has assigned to it.
- Making a status scannable in a list, a table or a grid, where reading each row in full is not realistic.
- Summarising a surface whose status the reader would otherwise have to infer from its contents.
Do not use it when
- The status needs explaining, or the reader needs to know what to do about it.Use
care-cardinstead. - The status must interrupt what the reader is doing rather than wait to be noticed.Use
alert-bannerinstead. - The label is a category, a source, a tag, a count or a release phase rather than a level of attention.Use
badgeinstead. - You want to show where a value sits relative to a range, which is a position rather than a level.Use
range-barinstead. - The message is informational and carries no clinical level at all.Use
calloutinstead.
Clinical meaning
Asserts. That the product has assigned one of exactly four levels of attention to
the thing the pill labels. The levels are steady, watch, attention and urgent,
defined in Clinical status semantics. It does
not say what is wrong, why, or what happens next.
Never read as. A diagnosis, a severity score or a triage category. The levels
describe what the interface asks of the reader, not the state of their body. urgent
on a screen is not urgent in a clinical handover.
Colour axis. Status only, and never alone: every pill renders a word and a glyph beside its colour. A pill never carries a category colour. That is a Badge under Category identity, and The two colour axes says why the two never share an element.
Thresholds. None. The consuming product assigns the level from rules or a clinician, and the pill derives nothing.
Anatomy
StatusPillThe root. Carries data-status and the level's surface, line and ink. Not focusable and not interactive.Controlled bystatusStatusPill.IconOne distinct glyph per level, from CLINICAL_STATUS_META[level].icon. aria-hidden and sized in em.StatusPill.LabelThe level in words. Not removable; an empty label raises OPSIN-0002 and falls back to the level's word.Controlled bylabel
Examples
Scanning a column
Four rows, each graded by status, and exactly one urgent. A pill earns its place
when the alternative is reading every row.
status-pill-in-a-list · base base · style base-lyraOpen under the product themeEvery size keeps the word
size sets the type step and padding and nothing else. sm and md label a status
in prose and lists; lg matches a heading. No size drops the word.
status-pill-sizes · base base · style base-lyraOpen under the product themeContent guidelines
Use the level's own word, and use the same word on every screen. Never write a level
as a sentence about the person: "Needs attention" labels a result, not a human being.
A banned word passed as label raises OPSIN-0006 in development;
Reference ranges and normal holds the list.
Do
"Steady" for a result that needs nothing right now.
Don’t
"Normal", "Healthy", "Good", "All clear" are claims about the person.
Accessibility
Audited against WCAG 2.2 AA in a source pass and a rendered pass. The audit is author-run, it is not an independent review, and clinical review is still pending. The rendered pass reported no axe violations, no target-size hits, no focus problems and no reflow break at 320px, and found nothing to change.
Checked by pnpm run check:a11y on every commit.
- No file paints a status colour without reading
CLINICAL_STATUS_METAand a glyph, and every status surface carriesdata-status. - The four levels name four distinct glyphs: circle, ringed dot, diamond and octagon. A11Y017 fails the build when two levels share one.
- The status words are read from
CLINICAL_STATUS_META[level].word, never written as a literal.
Argued from the source.
- Status is a visible word plus one of four distinct glyph shapes plus colour, never
colour alone. The icon is
aria-hiddenand sized inem, so the word carries the meaning and the glyph grows with it. - With
describes, the pill announces as Needs attention, Second example measurement. Omit it when the subject is visible text in the same reading unit. When the pill floats free of its subject, in a table cell reached by column navigation or a card corner, the consuming product must passdescribes, because the component cannot know the reading context. That is by design, and the prop JSDoc records it. - The pill is a
span, not focusable and not in the tab order. A pill made into a filter is a Button and owes a Button's target and name. - No live region is mounted, on purpose. The pill is a static read-only label rather
than a message, so a
role="status"announcement would be wrong here. - At 200% text the pill grows and wraps; the word is never truncated and never replaced by the icon alone.
Not yet measured. The rendered pass ran in the light theme and reported no contrast violation, so light-mode text contrast passes. Dark-mode text contrast, forced colours, and the non-text contrast of the boundary and glyph against the surface were not measured. Those pairs arrive through role tokens and live in the token layer and the report below, not in this source.
| Key | Action | Notes |
|---|---|---|
| Tab | Skips the pill | Not focusable. A label with a tab stop is a stop every row pays for. |
| Shift+Tab | Skips the pill | Same in reverse. Never in the focus order in either direction. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| attention ink on attention surface | light | 89.4 | 10.35:1 | Pass |
| attention ink on attention surface | dark | -81.1 | 12.54:1 | Pass |
| attention line on attention surface | light | 70.0 | 4.90:1 | Pass |
| attention line on attention surface | dark | -65.5 | 9.54:1 | Pass |
| attention accent on attention surface | light | 65.1 | 4.18:1 | Pass |
| attention accent on attention surface | dark | -49.4 | 6.80:1 | Pass |
| attention ink on the attention band | light | 22.4 | 2.48:1 | Below floor |
| attention ink on the attention band | dark | -31.0 | 1.84:1 | Below floor |
| attention accent on the page | light | 67.8 | 4.35:1 | Pass |
| attention accent on the page | dark | -50.5 | 7.47:1 | Pass |
| attention ink on the page | light | 92.2 | 10.77:1 | Pass |
| attention ink on the page | dark | -82.3 | 13.76:1 | Pass |
| steady ink on steady surface | light | 88.3 | 9.62:1 | Pass |
| steady ink on steady surface | dark | -82.9 | 12.56:1 | Pass |
| steady line on steady surface | light | 67.5 | 4.45:1 | Pass |
| steady line on steady surface | dark | -68.9 | 9.91:1 | Pass |
| steady accent on steady surface | light | 56.4 | 3.21:1 | Pass |
| steady accent on steady surface | dark | -53.2 | 7.25:1 | Pass |
| steady ink on the steady band | light | 30.0 | 3.00:1 | Below floor |
| steady ink on the steady band | dark | -28.8 | 1.73:1 | Below floor |
| steady accent on the page | light | 58.6 | 3.31:1 | Pass |
| steady accent on the page | dark | -54.5 | 8.19:1 | Pass |
| steady ink on the page | light | 90.6 | 9.94:1 | Pass |
| steady ink on the page | dark | -84.2 | 14.20:1 | Pass |
| unknown ink on unknown surface | light | 89.5 | 9.98:1 | Pass |
| unknown ink on unknown surface | dark | -82.2 | 12.56:1 | Pass |
| unknown line on unknown surface | light | 69.2 | 4.64:1 | Pass |
| unknown line on unknown surface | dark | -67.2 | 9.72:1 | Pass |
| unknown accent on unknown surface | light | 43.1 | 2.29:1 | Below floor |
| unknown accent on unknown surface | dark | -51.3 | 7.05:1 | Pass |
| unknown accent on the page | light | 45.6 | 2.37:1 | Below floor |
| unknown accent on the page | dark | -52.3 | 7.84:1 | Pass |
| unknown ink on the page | light | 92.0 | 10.34:1 | Pass |
| unknown ink on the page | dark | -83.1 | 13.97:1 | Pass |
| urgent ink on urgent surface | light | 88.2 | 10.58:1 | Pass |
| urgent ink on urgent surface | dark | -80.3 | 12.52:1 | Pass |
| urgent line on urgent surface | light | 70.0 | 5.09:1 | Pass |
| urgent line on urgent surface | dark | -64.8 | 9.52:1 | Pass |
| urgent accent on urgent surface | light | 72.3 | 5.58:1 | Pass |
| urgent accent on urgent surface | dark | -48.0 | 6.63:1 | Pass |
| urgent ink on the urgent band | light | 13.9 | 1.90:1 | Below floor |
| urgent ink on the urgent band | dark | -31.5 | 1.89:1 | Below floor |
| urgent accent on the page | light | 75.3 | 5.84:1 | Pass |
| urgent accent on the page | dark | -49.5 | 7.24:1 | Pass |
| urgent ink on the page | light | 91.2 | 11.06:1 | Pass |
| urgent ink on the page | dark | -81.8 | 13.66:1 | Pass |
| watch ink on watch surface | light | 89.4 | 10.02:1 | Pass |
| watch ink on watch surface | dark | -82.1 | 12.58:1 | Pass |
| watch line on watch surface | light | 69.5 | 4.72:1 | Pass |
| watch line on watch surface | dark | -66.8 | 9.68:1 | Pass |
| watch accent on watch surface | light | 61.7 | 3.72:1 | Pass |
| watch accent on watch surface | dark | -50.7 | 6.95:1 | Pass |
| watch ink on the watch band | light | 25.7 | 2.70:1 | Below floor |
| watch ink on the watch band | dark | -30.6 | 1.81:1 | Below floor |
| watch accent on the page | light | 64.2 | 3.85:1 | Pass |
| watch accent on the page | dark | -51.7 | 7.71:1 | Pass |
| watch ink on the page | light | 91.9 | 10.38:1 | Pass |
| watch ink on the page | dark | -83.1 | 13.95:1 | Pass |
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
| attention surface on the card material | light | 0.0 | 1.09:1 | Below floor |
| attention surface on the card material | dark | 0.0 | 1.00:1 | Below floor |
| steady surface on the card material | light | 0.0 | 1.08:1 | Below floor |
| steady surface on the card material | dark | 0.0 | 1.03:1 | Below floor |
| urgent surface on the card material | light | 0.0 | 1.09:1 | Below floor |
| urgent surface on the card material | dark | 0.0 | 1.01:1 | Below floor |
| watch surface on the card material | light | 0.0 | 1.08:1 | Below floor |
| watch surface on the card material | dark | 0.0 | 1.01:1 | Below floor |
| the focus ring on the attention surface | light | 67.3 | 4.44:1 | Pass |
| the focus ring on the attention surface | dark | -45.9 | 6.30:1 | Pass |
| attention ink on the neutral hover fill | light | 88.6 | 10.23:1 | Pass |
| attention ink on the neutral hover fill | dark | -79.3 | 10.53:1 | Pass |
| the focus ring on the steady surface | light | 67.8 | 4.47:1 | Pass |
| the focus ring on the steady surface | dark | -45.8 | 6.12:1 | Pass |
| steady ink on the neutral hover fill | light | 87.0 | 9.43:1 | Pass |
| steady ink on the neutral hover fill | dark | -81.3 | 10.86:1 | Pass |
| the focus ring on the urgent surface | light | 67.0 | 4.42:1 | Pass |
| the focus ring on the urgent surface | dark | -45.5 | 6.34:1 | Pass |
| urgent ink on the neutral hover fill | light | 87.7 | 10.50:1 | Pass |
| urgent ink on the neutral hover fill | dark | -78.9 | 10.46:1 | Pass |
| the focus ring on the watch surface | light | 67.6 | 4.46:1 | Pass |
| the focus ring on the watch surface | dark | -46.0 | 6.24:1 | Pass |
| watch ink on the neutral hover fill | light | 88.3 | 9.86:1 | Pass |
| watch ink on the neutral hover fill | dark | -80.1 | 10.67:1 | Pass |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
Data attributes
| Attribute | Condition | Value |
|---|---|---|
| data-slot | On the root and both inner parts, always | status-pill, status-pill-icon, status-pill-label |
| data-status | On the root, always, because status is required | steady | watch | attention | urgent |
API reference
Prop
Type
Generated from StatusPillProps in registry/bases/base/status-pill.tsx.
status has no default. A value outside the four, including unknown, renders no
pill and raises a development warning. className is merged last and can strip the
fill or the line, so change size instead. It cannot recolour the word; a rule on
[data-slot="status-pill"] can.
Related
- Badge is a label, not a judgement. Categories, sources, counts and release phases are badges.
- AlertBanner shares the vocabulary but interrupts. A pill waits to be noticed.
- CareCard is the status with an instruction attached, when the reader has to act.
- Callout sits outside the status axis, for information with no clinical level.
MetricTile
A compact readout of one measurement. It carries the number, its unit and how recent it is, and it is sized to sit in a grid with several others.
RangeLegend
The key that names the tones a RangeBar draws, so a reader learns in one place what the reference band and each status colour mean. It asserts nothing itself.