---
title: "StatusPill"
description: "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."
url: "https://opsinjs.pensievelabs.org/components/status-pill"
source: "https://opsinjs.pensievelabs.org/components/status-pill.md"
section: "Components"
status: "shipped"
kind: "component"
category: "health-data-display"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["chip", "status chip", "status badge", "status label", "traffic light"]
governedBy: ["two-colour-axes", "clinical-status-semantics", "alarm-fatigue", "category-identity"]
usedIn: ["alert-escalation", "choose-a-component", "health-metric-card", "offline-and-stale-data", "result-disclosure", "results-screen", "staged-alert", "trend-review", "value-against-a-range"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="status-pill"
  status="shipped"
  questions="[
  &#x22;When a pill's level changes on screen, what is announced and how urgently? The component mounts no live region today, on purpose.&#x22;,
  &#x22;Whether the pill's surface and boundary hold up under prefers-contrast: more and in forced colours; neither response lives in the token layer today.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="status-pill" />

## Installation [#installation]

<ComponentInstall name="status-pill" unbuilt="false" importPath="@/components/ui/status-pill" dependencies="[&#x22;lucide-react&#x22;]" />

## Usage [#usage]

```tsx
import { StatusPill } from "@/components/ui/status-pill"
```

```tsx
<StatusPill status="attention" describes="Second example measurement" />
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;Labelling one result, reading or item with the level of attention the product has assigned to it.&#x22;,
  &#x22;Making a status scannable in a list, a table or a grid, where reading each row in full is not realistic.&#x22;,
  &#x22;Summarising a surface whose status the reader would otherwise have to infer from its contents.&#x22;,
]"
  avoid="[
  { case: &#x22;The status needs explaining, or the reader needs to know what to do about it.&#x22;, instead: &#x22;care-card&#x22; },
  { case: &#x22;The status must interrupt what the reader is doing rather than wait to be noticed.&#x22;, instead: &#x22;alert-banner&#x22; },
  { case: &#x22;The label is a category, a source, a tag, a count or a release phase rather than a level of attention.&#x22;, instead: &#x22;badge&#x22; },
  { case: &#x22;You want to show where a value sits relative to a range, which is a position rather than a level.&#x22;, instead: &#x22;range-bar&#x22; },
  { case: &#x22;The message is informational and carries no clinical level at all.&#x22;, instead: &#x22;callout&#x22; },
]"
/>

## Clinical meaning [#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](../health/clinical-status-semantics.mdx). 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](./badge.mdx)
under [Category identity](../health/category-identity.mdx), and
[The two colour axes](../health/two-colour-axes.mdx) 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.

<SafetyCallout severity="urgent" evidence="opinion">
  At most one `urgent` pill may be visible on a screen. If the product's rules produce
  two, the screen needs an [AlertBanner](./alert-banner.mdx) for the most serious one.
  See [Alarm fatigue](../health/alarm-fatigue.mdx).
</SafetyCallout>

## Anatomy [#anatomy]

<Anatomy
  name="status-pill"
  parts="[
  {
    name: &#x22;StatusPill&#x22;,
    describes: &#x22;The root. Carries data-status and the level's surface, line and ink. Not focusable and not interactive.&#x22;,
    prop: &#x22;status&#x22;,
  },
  {
    name: &#x22;StatusPill.Icon&#x22;,
    describes: &#x22;One distinct glyph per level, from CLINICAL_STATUS_META[level].icon. aria-hidden and sized in em.&#x22;,
  },
  {
    name: &#x22;StatusPill.Label&#x22;,
    describes: &#x22;The level in words. Not removable; an empty label raises OPSIN-0002 and falls back to the level's word.&#x22;,
    prop: &#x22;label&#x22;,
  },
]"
/>

## Examples [#examples]

### Scanning a column [#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.

<ComponentPreview name="status-pill-in-a-list" kind="example" align="start" />

### Every size keeps the word [#every-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.

<ComponentPreview name="status-pill-sizes" kind="example" align="start" />

## Content guidelines [#content-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](../health/reference-ranges.mdx) holds the list.

<DoDont>
  <DoDont.Do>
    **"Steady"** for a result that needs nothing right now.
  </DoDont.Do>

  <DoDont.Dont>
    **"Normal"*&#x2A;, &#x2A;*"Healthy"*&#x2A;, &#x2A;*"Good"*&#x2A;, &#x2A;*"All clear"** are claims about the person.
  </DoDont.Dont>
</DoDont>

## Accessibility [#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_META` and a glyph,
  and every status surface carries `data-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-hidden` and sized in `em`, 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 pass `describes`, 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.

<KeyboardTable
  name="status-pill"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Skips the pill&#x22;,
    notes: &#x22;Not focusable. A label with a tab stop is a stop every row pays for.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Skips the pill&#x22;,
    notes: &#x22;Same in reverse. Never in the focus order in either direction.&#x22;,
  },
]"
/>

<ContrastReport component="status-pill" />

## Data attributes [#data-attributes]

<DataAttributesTable
  name="status-pill"
  rows="[
  {
    attribute: &#x22;data-slot&#x22;,
    condition: &#x22;On the root and both inner parts, always&#x22;,
    value: &#x22;status-pill, status-pill-icon, status-pill-label&#x22;,
  },
  {
    attribute: &#x22;data-status&#x22;,
    condition: &#x22;On the root, always, because status is required&#x22;,
    value: &#x22;steady | watch | attention | urgent&#x22;,
  },
]"
/>

## API reference [#api-reference]

<PropsTable name="StatusPillProps" />

`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 [#related]

* [Badge](./badge.mdx) is a label, not a judgement. Categories, sources, counts and release phases are badges.
* [AlertBanner](./alert-banner.mdx) shares the vocabulary but interrupts. A pill waits to be noticed.
* [CareCard](./care-card.mdx) is the status with an instruction attached, when the reader has to act.
* [Callout](./callout.mdx) sits outside the status axis, for information with no clinical level.
