opsinjs

Overview

What makes opsinjs a health design system rather than a general-purpose component library, and which decisions it fixes on a product's behalf.

60 shipped

What this means

A generic component library will render the number 212 in red inside a rounded rectangle without objection. It holds no position on whether that number is a cholesterol reading, on whether red here means "bad" or "cardiovascular", on whether the person looking at it knows what the unit is, or on whether the app has just told someone they are ill in a colour they cannot distinguish, at 11pm, with no way to ask a question until Monday.

Health is the domain in which the distance between "renders correctly" and "is safe to ship" is greatest. The failure that matters here is a reader misreading a result they were not prepared for, rather than a broken layout, and of the two possible misreadings false reassurance is more dangerous than panic. Every rule in this section exists because a plausible, well-built, good-looking interface can produce that outcome with no visible sign of malfunction.

This pillar is doctrine rather than library code: a small number of decisions, argued in public, that constrain what the components are allowed to do. The components are downstream of it, and it is the section that is complete today while the components are not, which is the reason to read it before writing code.

The rule

opsinjs decides how health information is presented. It never decides what that information means.

Decisions opsinjs fixes

Six decisions sit on the presentation side of that boundary. Each one is fixed for every product built on the system, and the enforcement column records how much of the fixing the build performs today and how much still rests on a reviewer.

DecisionWhat is fixedFailure it preventsWhere it is enforcedDoctrine page
Two colour axesCategory identity and clinical status are independent channels with independent palettes, so a colour that says "this is a heart metric" can never also say "this is worrying"One hue carrying both identity and urgency, after which a reader can rely on neitherThe --opsin-category-* and --opsin-status-* namespaces are disjoint, and a component that accepts a status does not accept a category for the same surfaceThe two colour axes
Four clinical status levelssteady, watch, attention and urgent form an ordinal ladder in that order. Each level has a fixed meaning and a fixed tone, and urgent is budgeted at one surface per screenA fifth level invented for one screen, and an urgent treatment spent on a product problemEvery component takes the level from the exported ClinicalStatus union, so an invalid level is a type error rather than an unstyled element. The per-screen budget is held by reviewClinical status semantics
Status on more than one channelColour, shape, icon and a word carry a status together, every timeA status invisible to a reader with a colour vision deficiency, or on a greyscale displayStatusPill exposes no color prop, no variant prop and no way to remove the wordClinical status semantics
Number formattingPrecision, units and rounding are a correctness surface: one metric is shown to the same number of decimal places every time, and no number is rendered without its unitA reading rounded or padded into a claim the instrument did not makeValue takes a magnitude, a unit and a precision rather than a pre-formatted string, holds no default precision of its own, and puts the unit into the accessibility tree in wordsNumbers, units and precision
Rendered uncertaintyA measurement, an estimate, a stale reading and an absent value are four visually distinct statesA three-day-old reading presented as currentRelativeTime ships no staleness default, so a product that states no threshold gets no stale treatment, and its one default, absoluteAfterDays, chooses the wording for an age without carrying a staleness verdict. Value refuses to render an unknown as a numberUncertainty, staleness and missing data
The word "normal"The word is banned from any user-facing string that describes a person's own resultA statement about one measurement read as a verdict on the personcheck-a11y.mts gates the banned-word list from tokens/glossary.json across the component sources opsinjs ships. No check reaches a consuming product's strings. There the list falls to review, and the lint rule over user-facing strings has not been writtenReference ranges

Motion is never one of the channels that carry a status, which is why it is absent from the third row. A reader who has prefers-reduced-motion set receives the end state instead of the animation, so a signal carried in motion reaches some readers and not others. Motion in health UI sets out the seven requirements that follow from the rule that motion may support a message but may never be the message.

Decisions opsinjs withholds

Four decisions sit on the clinical side of the boundary. The system supplies none of them, and each one has a named owner outside it.

Decision withheldWho owns itWhy opsinjs cannot supply itWhere to look
The thresholds separating watch from attentionThe product, and whoever is clinically accountable for itA threshold is a claim about what a measurement means for a person, and a presentation layer that shipped one would be making that claim with no authorClinical status semantics
The reference rangesThe laboratory, device manufacturer or clinical team that issued themThey vary by laboratory, assay, age, sex, pregnancy and populationReference ranges
The words that carry clinical meaningThe product's clinical content owneropsinjs supplies sentence shapes, and the clinical content placed inside them belongs to the productClinical interaction guidelines
Whether the product is a medical deviceThe organisation shipping it, answering to its regulatorClassification turns on the intended purpose the product states for itself, which the system never seesRegulatory context, then Safety, scope and limitations

Why (evidence)

Every page in this section declares an evidence field and is bound by it: a page marked cited names a source the reader can open, and a page marked opinion states which parts we would change our mind about. The full register of sources is at Evidence and references. We have not, at the time of writing, had this pillar reviewed by an external clinician, and the open item below is where that gap is tracked.

Not written yet. External clinical review of the whole Health pillar. Until it happens, no page here may be described as clinically approved, and reviewer: clinical does not appear in any frontmatter in this directory.

Applying it

Two pages in this section carry the operational load. Clinical interaction guidelines states ten numbered prohibitions, each written so that a reviewer can test a screen against it, and Safety review checklist is one printable list, grouped into ten lettered sections, whose items name the doctrine page they come from where one exists.

Do

Read Clinical interaction guidelines before your first screen, then keep Safety review checklist open during review.

Don’t

Treat this section as background reading to come back to. The colour axes and the status vocabulary are structural decisions, and retrofitting either one means revisiting every screen that has already chosen for itself.

Reading order by task

The order below depends on what the implementing team is building today. The third column names a page that a condition adds, rather than a page everyone needs.

What the team is buildingRead in this orderRead additionally when
A screen that shows a measured valueReference ranges, then Numbers, units and precision, then Uncertainty, staleness and missing data
A surface that alertsClinical status semantics, then Alarm fatigue, then Emergency and escalation
Anything that asks the reader a questionConsent and disclosureAny answer could disclose risk of harm: Crisis and self-harm

Components that implement this

Every component listed here carries a governedBy field naming the doctrine pages it must satisfy. scripts/assert-ia.mts fails the build with CAT003 when a component's governedBy names a doctrine page whose implements does not list it. The reverse asymmetry, a doctrine page claiming a component that does not claim it back, is reported as a warning and does not fail pnpm check.

What this does not cover

This pillar is written for consumer and patient-facing interfaces: an app someone uses to look at their own health data, log something about themselves, or be told something about a result. The four domains below sit outside that scope, the reason differs in each, and this guidance should not be used as-is in any of them.

Out of scopeExamplesWhy this guidance does not transferWhat applies instead, or who is accountable
Clinician-facing softwareElectronic health records, prescribing, triage tooling, diagnostic workstationsThe reader is trained, the density requirements are the opposite of the ones here, and the safety analysis is different
Regulated device user interfacesAnything that is part of a medical device under EU MDR, UK MDR or the FDA's device frameworkClassification determines what the interface must do and must prove, and opsinjs confers no regulatory status of any kindRegulatory context
Clinical contentThresholds, ranges, dosing, triage logic, and any sentence whose truth is a clinical claimThe truth of the sentence is a clinical question rather than a presentation oneWhoever is clinically accountable in your organisation
Legal adviceWhether a regime applies to a given product, and what it requires once it doesRegulatory context is a starting map written by engineers and designers rather than by lawyers

Updates to this page

Last read through against the system on 2026-09-02. Due for review every 6 months; expiry is reported by pnpm run check:freshness.

On this page