Status palettes
Why the four clinical status ramps are the one part of the token system opsinjs asks you not to redefine, and what to do when you have to anyway.
Overview
Every other tier-1 ramp in opsinjs is a starting point. The status ramp is not.
It has four ordinal levels, whose token ids are steady, watch, attention
and urgent. A fifth stem, unknown, is the absence of a verdict rather than a
fifth degree of one. This page argues that you should leave all of them
alone, then tells you how to change them safely if that argument does not survive
contact with your organisation.
What the four levels mean, who may assign one, and what word a reader
actually sees are decided in
Clinical status semantics, which is
canonical and which this page does not restate. The token id is a machine
identifier and it is not the label: attention is the name of a ramp, "Needs
attention" is what the reader sees, and no patient should ever be shown the raw
id.
Why this axis is closed
The status ramp is not a palette. It is a vocabulary, and it has at least four consumers that never talk to each other.
- Your product. A surface, a line, an ink colour, and a
data-statusattribute on the element. - This documentation.
<Callout>,<StatusLadder>and<StatusBadge>are bound to the same four levels, deliberately, so that the docs chrome and the product chrome share one status vocabulary rather than shipping two. - The registry. Every catalogue row and every registry item carries status metadata using these ids.
- Agents. Rules for agents instructs a model to reach for a status level and never for a colour. That instruction is only safe while the ids mean one thing.
Redefining the ramp locally breaks the last two silently. Your app renders correctly, your generated code does not, and nothing errors.
There is a second, blunter reason. A status is a claim about somebody's health data. Changing the appearance of that claim to fit a brand is a decision with a clinical consequence and no clinical review attached to it, which is the exact shape of mistake this system exists to make harder.
What you may change without breaking anything
Three things are yours, and none of them are the ramp.
The sentence around the word rather than the word itself. The four status
words are fixed in lib/status.ts, and a component reads them from
CLINICAL_STATUS_META rather than taking them as a prop, for the same reason
the ramp is closed: four consumers that never talk to each other must not end
up saying four different things. What is yours is everything else in the
sentence: the measurement, the comparison, the single action. That is a content
decision governed by
Writing status and alerts, not a
token decision, and changing it costs nothing structurally.
The thresholds. Which value maps to which level is your product's business logic, and opsinjs deliberately owns none of it. See Reference ranges.
Which levels you use. Nothing requires all four. A logging app that never
escalates may only ever render steady and watch. Using two of four is
correct; inventing a third meaning for one of them is not.
When you must change the ramp
Two legitimate reasons come up repeatedly.
Your organisation has a mandated clinical colour standard. Some health systems do, and it outranks a design system. In that case you are conforming to a standard, not rebranding, and you should say so in your own documentation so the next developer does not "fix" it back.
The default ramp fails for your users. An increased-contrast requirement, a display technology, or a printing constraint the defaults did not anticipate. This is a real reason and the system would rather you changed the ramp than shipped something illegible.
Neither reason is "our brand is blue".
Change it safely
Change the source, not the CSS
Edit the status entries in tokens/color.json and run:
pnpm run generateapp/tokens.generated.css is generated and diffed; a hand edit there is
reverted by pnpm run check:generated.
Preserve the ordering
The four levels are ordinal. Whatever you choose, urgent must read as more
urgent than attention, which must read as more urgent than watch. If your
palette makes watch the loudest thing on the screen, you have kept the ids and
inverted the meaning, which is worse than having no status colour at all.
Preserve the distance
Adjacent levels must be distinguishable from each other, and every level must be
distinguishable from all six category ramps. Check it under
<CvdSimulator> in grayscale as well as in the three dichromacies. Grayscale
is the honest proxy for a printed page and a bright screen outdoors.
Re-measure everything
pnpm run contrastEvery surface/ink and surface/line pair, in light and in dark, against the published floor. This is not optional: the numbers published on this site describe the shipped ramp and stop being true the moment you change it. See Validating your theme.
Write down that you did it
Record the change where your team will find it, and mention it in any accessibility statement you publish. A conformance claim inherited from opsinjs does not survive a retuned status ramp.
Do not add a fifth level
Do
Map your extra state onto the existing four and carry the nuance in the
word. "Slightly above your usual range" and "Above your usual range" are
both watch; they differ in copy, not in colour.
Don’t
Add --opsin-status-critical-*. Four ordinal levels is a decision, not an
accident: five levels is more than a person can reliably rank by colour, and
the fifth exists only in your codebase. The registry, the docs and every
agent will map it to something else or drop it.
If four genuinely cannot express what your product needs, the problem is probably that you are using status to carry something that is not urgency, such as a data quality, a staleness or a device error. Those have their own homes: Uncertainty and staleness and Data states.
Verify it worked
The ladder still climbs
Render the four levels side by side, in order, in both themes, and ask somebody who has not seen your palette to sort them by urgency. If they cannot, the change failed regardless of what the contrast numbers say.
Every pair still passes
pnpm run contrast reports zero failures against the floor, in light and dark,
including the Display-P3 escalation.
Nothing else moved
git diff should touch tokens/color.json and the generated files only. If a
category ramp changed, you edited a shared rung.
Troubleshooting
Two levels look the same in dark mode. Dark ramps have less headroom. Separate them by lightness rather than by chroma; chroma differences collapse first under low ambient light and under simulation.
The docs chrome and my product disagree about what attention looks like. They
are meant to: the chrome renders the docs theme and previews render the product
theme in a separate document. That is Lyra and the docs
chrome, not a bug.
Contrast passes but the surface looks muddy against a card. Status surfaces are tuned against the page background, not against every material rung. Check which rung you have put it on. See Choosing a layer.
An agent generated a hex colour instead of a status. Expected, if the agent did not have the rules. Point it at Rules for agents or install the skill.
Next
- Clinical status semantics says what the four levels mean. Canonical.
- Validating your theme covers proving a retuned ramp is still legible.
- Alarm fatigue explains why the loudest level being rare is a design property and not a happy accident.