- Accordion
- Alert
- Alert Dialog
- Avatar
- Badge
- Button
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Circular Progress
- Collapsible
- Compare Slider
- Dialog
- Dropdown Menu
- Gauge
- Input
- Label
- Marquee
- Navigation Menu
- Number Field
- Pagination
- Popover
- Progress
- Radio Group
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Tooltip
Demos
A result inside its range.
"use client"
import { ReferenceRange } from "@workspace/ui/tools/reference-range"
export function ReferenceRangeDemo() {
return (
<div className="mx-auto flex w-full justify-center">
<ReferenceRange
name="Glucose, fasting"
value={92}
unit="mg/dL"
range={{ low: 70, high: 99 }}
/>
</div>
)
}
Usage
Use a reference range card to show one lab result: the value, its flag, the range printed on the report, and where the value falls. For a full report, put several in a grid.
Import it from @workspace/ui/tools/reference-range.
Anatomy
- Header — The test name, an optional longer name, and a flag badge.
- Value — The result and its unit, in the status colour.
- Range — The reference range as printed on a report, such as
Ref 70–99 mg/dL. - Bar — Coloured zones with a marker at the value: normal inside the range, mild outside it, and extreme past a critical limit. Only shown for numbers with a low or high limit. The harmless side of a directional range is grey.
- Footer — The Z-score and the direction, when there are any.
Flags
| Flag | When |
|---|---|
normal | Between low and high, inclusive. |
low, high | Outside low or high. |
critical-low, critical-high | Below criticalLow or above criticalHigh. Checked first. |
abnormal | Only when you pass flag="abnormal", such as a text result that doesn't match. |
unflagged | A text value, or no low or high. |
Z-score and direction
With both limits, the range is read as the middle 95% of healthy values. The mean is (low + high) / 2, the SD is (high − low) / 4, and Z is (value − mean) / SD. A value inside the range has a |Z| of 2 or less.
range.direction says which way is bad:
| Direction | Label | Meaning |
|---|---|---|
high | ↑ Bad | A raised value is a concern, such as CRP. A low one is not. |
low | ↓ Bad | A lowered value is a concern, such as HDL or vitamin D. |
both | Normal best | Either side is a concern. The default. |
context | Context | Either side may matter, depending on the person. |
rangeDirectionLabels maps each direction to its label.
Colours
The colour uses the five-step severity scale from globals.css, in the same order as the Bio Clock. Change --status-* in your theme to recolour it.
| Result | Colour |
|---|---|
| In range | --status-normal |
| Out of range, |Z| under 3 (or no Z-score) | --status-mild |
| |Z| 3 to under 4 | --status-moderate |
|Z| 4 to under 5, or abnormal | --status-severe |
| |Z| 5 or more, or a critical flag | --status-extreme |
| Out of range on the harmless side, or unflagged | Muted |
Notes
- Limits are inclusive. A glucose of 99 with a high of 99 is normal.
- A range with only
highprints as≤ 200, and its bar starts at zero. A range with onlylowprints as≥ 30. range.textreplaces the printed range, for results such asNegative. Text values are never flagged on their own, so passflagyourself.decimalssets the decimal places for the value and the range.- The card sets
data-flaganddata-tone, for styling or tests.
Reference range helpers
The flagging logic is in @workspace/ui/lib/reference-range, so you can use it without the UI.
rangeTone(flag, z, direction) from the component file returns the status colour the card uses, as a RangeTone: normal, mild, moderate, severe, extreme, or none.
Examples
Flags
Low, high, critical, a lower limit only, a text result, and a result with no range.
"use client"
import { ReferenceRange } from "@workspace/ui/tools/reference-range"
export function ReferenceRangeFlags() {
return (
<div className="mx-auto grid w-full max-w-3xl gap-3 sm:grid-cols-2">
<ReferenceRange
name="Haemoglobin"
value={11.2}
unit="g/dL"
decimals={1}
range={{ low: 12, high: 15.5 }}
/>
<ReferenceRange
name="Total cholesterol"
value={232}
unit="mg/dL"
range={{ high: 200 }}
/>
<ReferenceRange
name="Potassium"
value={6.8}
unit="mmol/L"
decimals={1}
range={{ low: 3.5, high: 5.1, criticalLow: 2.8, criticalHigh: 6.2 }}
/>
<ReferenceRange
name="Vitamin D"
value={34}
unit="ng/mL"
range={{ low: 30 }}
/>
<ReferenceRange
name="Urine protein"
value="Positive"
range={{ text: "Negative" }}
flag="abnormal"
/>
<ReferenceRange name="Blood group" value="O+" />
</div>
)
}
Interactive
Change the value to see the flag and the marker move. Potassium has critical limits at 2.8 and 6.2 mmol/L.
"use client"
import * as React from "react"
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldScrubArea,
} from "@workspace/ui/components/number-field"
import { ReferenceRange } from "@workspace/ui/tools/reference-range"
export function ReferenceRangeInteractive() {
const [value, setValue] = React.useState<number | null>(4.2)
return (
<div className="mx-auto flex w-full max-w-sm flex-col gap-3">
<NumberField
value={value}
onValueChange={setValue}
step={0.1}
format={{ minimumFractionDigits: 1, maximumFractionDigits: 1 }}
>
<NumberFieldScrubArea label="Potassium (mmol/L)" />
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
<ReferenceRange
name="Potassium"
value={value ?? Number.NaN}
unit="mmol/L"
decimals={1}
range={{ low: 3.5, high: 5.1, criticalLow: 2.8, criticalHigh: 6.2 }}
/>
</div>
)
}
Biomarker panel
Forty example biomarkers, each with a direction. Most are out of range, so the colours step up with |Z|. CRP at 4.2 is inside its range, so it stays green even with a Z of +1.42.
"use client"
import { biomarkerRange, biomarkers } from "@workspace/ui/lib/biomarkers"
import { ReferenceRange } from "@workspace/ui/tools/reference-range"
// Example results, by biomarker id.
const results: Record<string, number> = {
"hs-crp": 3.5, crp: 4.2, nlr: 3.2, "il-6": 9.8, "tnf-alpha": 12.7,
"ck-mm": 190, "aldolase-a": 8.5, "igf-1": 82.8, myostatin: 18.3,
"mmp-9": 155.8, "ctx-ii": 510.7, pth: 75.3, "ca-p-ratio": 2.9,
"vitamin-d": 25.4, osteocalcin: 8.9, pinp: 9.7, fbs: 110, fsi: 28.8,
hba1c: 7.6, "total-cholesterol": 225.6, "ldl-c": 185.5, "hdl-c": 35.7,
triglycerides: 165.8, bdnf: 12.8, vegf: 305.6, nox: 18.9, ngf: 18.8,
"substance-p": 225.6, gfap: 125.7, "et-1": 4.6, mda: 4.6,
"tgf-beta1": 12.7, "timp-1": 55.9, egfr: 82.7, creatinine: 1.8,
bun: 21.8, "uric-acid": 8.9, urea: 45.6, albumin: 2.8, comp: 850,
}
export function ReferenceRangePanel() {
return (
<div className="mx-auto grid w-full gap-3 sm:grid-cols-2 lg:grid-cols-3">
{biomarkers.map((biomarker) => (
<ReferenceRange
key={biomarker.id}
name={biomarker.abbreviation}
description={biomarker.name}
value={results[biomarker.id] ?? Number.NaN}
unit={biomarker.unit}
range={biomarkerRange(biomarker)}
className="max-w-none"
/>
))}
</div>
)
}
Props
Unlisted props pass through to the Card that wraps the result.
ReferenceRange
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | The test name, such as Glucose or hs-CRP. |
description | string | — | A longer name under the title. |
value | number or string | — | The result. |
unit | string | — | Unit shown after the value and the range. |
range | ReferenceRange | — | The limits to flag against. |
decimals | number | — | Decimal places for the value and the range. |
flag | RangeFlag | computed | Overrides the flag. |
className | string | — | Classes merged onto the card. |
ReferenceRange (type)
| Field | Type | Description |
|---|---|---|
low | number | Lowest normal value, inclusive. |
high | number | Highest normal value, inclusive. |
criticalLow | number | Values below this are critical low. |
criticalHigh | number | Values above this are critical high. |
text | string | Shown instead of the generated range, such as Negative. |
direction | "high", "low", "both", or "context" | Which way is bad. Defaults to both. |