Show a value against a reference range
Put one measurement next to the range it is compared with, without implying a diagnosis, a verdict or a target.
The task
A person has a measurement and a range it is usually compared against. Your job is to show where the measurement falls, in a way that is true, legible at a glance, and readable by somebody who is anxious, distracted, or reading it on a phone in a car park outside a clinic.
This is the highest-stakes display in the system, and it fails in a specific way. It is trivially easy to build something that looks like a verdict: a bar that goes green in the middle and red at the ends, a value that "passes", a range labelled normal. None of those are what a reference range is. A reference range is a statistical statement about a population. It is usually the central band of results from a reference group, measured on a particular instrument by a particular laboratory. It is not a statement about whether you are well, and a value outside it is not a diagnosis.
What you need
The ten components this recipe composes are all built, installable and
shipped: the code exists and installs, and the API may change in any release.
Every opsinjs component has been audited against WCAG 2.2 AA by its own authors;
see ADR 0025. None has had
an independent accessibility review, and none has had a clinical review.
shipped means the source installs and passed that author-run audit, and it does
not mean either independent review has happened. Nothing here is for a production
health surface until a clinician signs it.
| Part | Component | Status | What it does here |
|---|---|---|---|
| The range display | RangeBar | Shipped | The band, the marker and the endpoint labels. |
| The whole result | ResultCard | Shipped | Value, range, meaning and next step as one unit. |
| The number | Value | Shipped | Precision, unit adjacency, unit system. |
| The status, if assigned | StatusPill | Shipped | Separate from the bar. The bar shows position; the pill shows judgement. |
| Unfamiliar words | Term | Shipped | Plain-English expansion from the glossary. |
| The boundary | DisclaimerNote | Shipped | The governed not-medical-advice note, placed by rule. |
| What to do | CareCard | Shipped | Only when there is a real action with real timing. |
| When it was taken | RelativeTime | Shipped | A result with no date is not a result. |
| Endpoint labels | RangeLegend | Shipped | The key to the bands the bar draws, sat beside it. |
| Where it came from | SourceCitation | Shipped | Where the reading came from, and when it was last checked. |
Tokens, by family:
| Family | Where it applies | Reference |
|---|---|---|
--opsin-status-* | The status pill and, if used at all, the band edge treatment | Clinical status semantics |
--opsin-category-* | The result's identity in a list of results. It never colours the band | Category identity |
| Contrast floor | The marker against the band, in both themes | The contrast floor |
| Type scale | The value, which is the largest thing on the card | Type scale |
Build it
Establish where the range came from
Before you draw anything, write down four facts: the source of the range, the population it describes, the units it is expressed in, and the date or version of the source. If you cannot fill in all four, you do not have a reference range. You have a pair of numbers, and drawing them as a range asserts an authority nobody granted.
This is a product responsibility, not a component one. opsinjs never supplies thresholds, ranges or cut-offs, and a component that appeared to would be a defect. See Reference ranges for the boundary.
Convert units once, at the edge
Decide the canonical unit for the measurement, convert on the way in, and store that. Never convert in the view. A range in mmol/L drawn against a value in mg/dL is off by a factor of eighteen and looks entirely plausible.
Unit systems treats this as a correctness surface rather than a localisation one, for exactly this reason. When the reader switches unit system, the value, both endpoints and the marker position all move together or none of them do.
Draw position, not verdict
The bar's job is to answer "where does my number sit relative to this band?" and nothing else. Concretely:
- The band is one continuous shape with visible endpoints. It is not divided into coloured zones, because zones read as grades.
- The marker is a distinct shape placed at the value, and a colour change alone is not one. It must be findable in greyscale.
- The axis is not truncated to make a small difference look large.
- If the value is far outside the band, the axis extends or the marker pins to the edge with an explicit "beyond the scale" treatment. It never silently clamps, because a clamped marker reads as just outside when it may be far outside.
Keep the judgement out of the bar
If your product's clinical rules assigned a status, it appears as a StatusPill beside the bar, with its word, its icon and its colour. It does not recolour the band, the marker or the card.
Two reasons. First, the never-mix rule: the bar is a measurement display and the pill is a status display, and merging them makes the status impossible to remove when no rule assigned one. Second, honesty: a great many results have no assigned status, and a design that only looks finished when it is coloured will get coloured.
Label the endpoints in words
The numeric endpoints are necessary and not sufficient. Add the sentence that says what the band is: "The usual range for this test in adults is 3.9 to 5.6 mmol/L." That sentence is the accessible description, the print fallback and the thing a reader repeats to somebody else.
Never label the band normal. Use usual range, reference range, or the range this test is compared against. The ban and its reasoning are in Reference ranges.
Write the text alternative before the graphic
The bar is an enhancement. Write the sentence that carries the entire meaning first, then check that the graphic adds speed rather than information. If the graphic carries something the sentence does not, a screen-reader user, a print reader and a reader at 200% text are all missing it.
Alt text and descriptions covers describing dials, ranges and body maps non-visually.
Say what happens next even when nothing does
Every result surface answers "so what do I do?". For most results the honest answer is nothing, and here is when this will next be measured. Say it. A result screen that ends in silence is read as a screen that is withholding something, which produces exactly the anxiety the design is trying to avoid.
When there is a real action with real timing, it is a CareCard: what to do, by when, and what happens if it is not done.
The copy
The result sentence has a fixed shape. Fill it in; do not improvise it.
Your
<measurement>was<value> <unit>, measured<when>. The usual range for this test is<low>to<high><unit>.<What this does or does not mean, in one sentence.><What happens next, or that nothing needs to happen.>
Do
"Your fasting blood glucose was 6.1 mmol/L, measured on 14 March. The usual range for this test is 3.9 to 5.6 mmol/L. A single result above the range does not on its own mean you have a condition. Your clinic will repeat this test."
Don’t
"Glucose: 6.1 HIGH ⚠️ (Normal: 3.9 to 5.6). Your result is abnormal." Four faults: a banned word, a verdict the data does not support, shouting, and no statement of what happens next.
Banned words on this surface: normal, abnormal, good, bad, poor, failed, passed, dangerous, critical (unless it is the assigned status word and your product owns that rule). Each is banned because it asserts a judgement the range cannot support, and each has a replacement: describe the position ("above the usual range"), not the person.
Never use an exclamation mark, capitals, or an emoji to signal severity. Severity is carried by the status vocabulary and the surface it appears on. See Writing status and alerts.
Get it right
Safety
- The screen states where the range came from, or does not draw a range.
- The value and both endpoints are in the same unit, from the same conversion.
- Out of range is described as position, never as a verdict.
- No component computed the status. If a status is displayed, you can name the rule and the owner of that rule.
- Nothing on the screen implies a trend from a single measurement. One point is not a direction. See Trends and change.
- The disclaimer is placed by the rule in DisclaimerNote, not wherever it fits.
- If this result could be distressing, the way it is revealed matters as much as the way it is drawn: Result disclosure.
Accessibility
- The text alternative carries the whole meaning: value, unit, range, position, and what happens next.
- The marker is distinguishable from the band by shape and by contrast, verified in both themes against the floor published in Contrast conformance.
- The result survives greyscale and all three simulated colour-vision deficiencies. Colour independence sets that bar.
- At 200% text the bar reflows or gives way to the sentence; it never truncates the value. Text resizing and zoom covers the reflow behaviour.
- The value never animates into place. A number that counts up is a number the
reader cannot read, and under
prefers-reduced-motionit must simply be present. Motion in health UI is the governing rule. - Any Term expansion is reachable by keyboard and does not depend on hover.
Variations
The range depends on the person. Age, sex, pregnancy, medication and assay all shift reference intervals. If your range is conditional, the condition is part of the label: "the usual range for adults over 18". If you cannot state the condition, you cannot state the range.
There is no range. Common, and a legitimate state. Show the value, say plainly that this test has no range to compare against here, and do not draw an empty bar. An empty bar reads as a missing result rather than as a missing range. This is the partial data state from Data states.
There are two bands. A reference range and a personal target are different claims and must not share an axis without being distinguished in words as well as visually. If you cannot make the distinction clear in one sentence, show one.
The value is a category, not a number. Detected/not detected, positive/negative. There is no range and no bar; this is a ResultCard with a value, a plain-English expansion of what the words mean, and what happens next.
The result is one of many. A panel of twenty results is a list, and a list of twenty bars is unreadable. Show the value and the position in words, and let the reader open the one they care about.
No recipe covers a full results panel end to end. That is the list-of-many case. The screen specimen closest to it is Results screen.
Related
- Reference ranges is the doctrine this recipe implements, including why normal is banned outright.
- RangeBar has the component specification, its proposed API and the accessibility bar it must clear.
- Result disclosure covers the flow around this screen, for results somebody may not want to see.