opsinjs
IntroductionStart here

Quick start

Build a result card that shows one value, its reference range and its clinical status, from three components that are implemented and installable.

Overview

The smallest complete opsinjs screen is one value, the range it should be read against, and a sentence saying whether the person needs to do anything. That is what ResultCard is for, and building it exercises almost every rule the system has: the two colour axes, the number formatting contract, the reference-range rule, and the accessibility floor.

Shipped components, and no published host yet

All three components below are implemented, and their registry items carry the full source that shadcn add copies. Two things are still absent. There is no npm package and there will not be one. Distribution is registry copy-in, and ADR 0002 is why. opsinjs.pensievelabs.org, the canonical host every command on this site names, is also not serving yet, so @opsinjs resolves only against a registry you point it at yourself. All three are shipped, so the code installs and the API may change in any release. None of them has been reviewed, which makes this fit for a prototype rather than for use in front of a patient. The ResultCard block further down is older than the implementation and still uses the names the specification proposed; the marked gap under it says which.

You will need a React 19 project with Tailwind v4 already working. If you do not have one, Installation covers each framework.

Add the pieces

Three registry items: the card itself, the range bar it composes, and the status pill that carries the clinical level. The command is a plain shadcn add against the @opsinjs namespace. opsinjs does not ship its own CLI, because the shadcn CLI already does this job. result-card names the other two as registry dependencies, so adding it alone brings them, along with the two lib modules every opsinjs component imports.

npx shadcn@latest add @opsinjs/result-card @opsinjs/range-bar @opsinjs/status-pill

That copies real source files into your project rather than adding a dependency. You own the result. See Registry for what the namespace resolves to and Upgrading for how you take changes to code you own.

Decide what the number means before you render it

This is the step that a general-purpose UI library does not have, and it is the one that determines whether your screen is honest.

Pick the reference range from a source you can name

Not from opsinjs, and not from a component default. The range depends on the assay, the laboratory, the population and often the person. Your product's clinical owner supplies it, and your UI should be able to say where it came from. Reference ranges covers what happens when there is no defensible range: you show the value without one.

Map the value to a clinical status, in your code

The four status levels are a fixed vocabulary with fixed meanings, and your product assigns them. A component never derives a status from a number. Clinical status semantics defines each level, who may assign it, and what it must never be read as.

Choose the category, independently

The category says which part of a person's health this is. It never says how concerning the reading is. Both attributes travel on the component and are rendered on different visual channels, which is what keeps them apart in the markup. There is no build-time rule refusing a category colour used to signal severity. The lint plugins that would do it are specified and not built, so this is a review check today. See The two colour axes.

Write the sentence

A number without a sentence makes the reader guess, and anxious readers guess badly. Writing status and alerts gives the sentence pattern for each status level and the words banned at each. That list starts with normal, which is banned everywhere.

Render it

The shape of the screen. Read the props off ResultCard before you copy this block. It was written against the proposal and has not been brought back into line with the component that now exists:

app/results/page.tsx
import { ResultCard } from "@/components/opsinjs/result-card"

export default function Page() {
  return (
    <ResultCard
      label="Blood pressure"
      category="heart"
      status="watch"
      value={{ systolic: 138, diastolic: 86 }}
      unit="mmHg"
      precision={0}
      range={{ low: 90, high: 120, source: "Your clinic, 2026" }}
      measuredAt="2026-09-01T08:12:00Z"
    >
      Slightly higher than your usual morning reading. One reading on its own
      does not mean much. Take another tomorrow at the same time.
    </ResultCard>
  )
}
Not written yet.

Reconcile this example with the shipped props on ResultCard. The card takes title rather than label, a compound reading such as a blood pressure through segments rather than an object in value, the explanation through meaning rather than as children, and a required now read once per screen so a page of cards cannot disagree with itself across a minute boundary. A range is drawn only against a single reading, so the corrected example has to choose between the pair and the bar rather than showing both.

Owner: engineering

Three things in that block are load-bearing, and all three survive the rename described above. The shipped component requires them under its own prop names, so they are enforced by the type checker rather than suggested by this page:

  • status and category are separate props. They cannot be collapsed into one "variant", because they answer different questions.
  • range carries its source. A range with no attributable source is a claim the interface cannot support, and the component surfaces the source rather than hiding it.
  • measuredAt is required, not optional. A health value with no timestamp is ambiguous in a way that matters: the reader cannot tell a reading from this morning from one taken six weeks ago. See Uncertainty, staleness and missing data.

The full interface, its defaults and its prohibitions live on the component page: ResultCard.

Verify it worked

These are the checks that decide whether your screen is correct, and they are worth running against a design before anything is built.

  • Switch the whole screen to greyscale. Every status distinction must still be legible from the icon, the word and the position. If greyscale loses the meaning, colour was doing work it is not allowed to do alone.
  • Set the system text size to 200%. Nothing may clip, truncate or scroll horizontally. The value stays the largest thing on the card.
  • Read the card aloud, in order, with your eyes shut. The announcement order is label, value, unit, status, then time. If the status arrives before the value, a screen-reader user hears the alarm before they hear what it is about.
  • Count the urgent surfaces on the screen. More than one is a defect. See Alarm fatigue.
  • Search the rendered copy for the word normal. It should not be there.

Troubleshooting

The command fails to resolve. opsinjs.pensievelabs.org is not serving yet, so a components.json pointing @opsinjs there fails before the CLI reaches a registry at all. See Troubleshooting, which separates that from the different failure of having no registries entry.

You want the component to pick the status for you. It will not, and this is the single hardest constraint for teams arriving from a general-purpose library. A component that derives severity from a number has made a clinical decision inside a rendering layer, where it cannot be reviewed, versioned or audited.

You have a value but no defensible range. Render the value without a range. An invented range is worse than no range: it looks authoritative and is not.

Your category and your status want the same colour. They are not allowed to share one. If your brand's heart colour is a red, the status axis still owns red, and Category palettes explains how to add a category without contaminating the status axis.

Next

  • Add your first component is the same thing in sixty seconds, with nothing else to read.
  • Choose a component has the decision table from the shape of your data to the component that displays it.
  • ResultCard is the full specification, including the parts this page skipped.

On this page