RangeBar
A single measurement drawn on a line, so you can see at a glance where your number sits compared with the range it is usually expected to fall in.
Preview
range-bar · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/range-bar
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { RangeBar } from "@/components/ui/range-bar"<RangeBar
label="Example measurement"
value={14}
unit="mg/dL"
precision={0}
locale="en-GB"
range={{ low: 10, high: 20, source: "Example data rather than a reference range" }}
/>When to use it
Use it when
- One measurement with a published reference range, shown to its owner.
- A value whose meaning is positional rather than absolute.
- A reader who would otherwise compare two numbers in their head.
Do not use it when
- There is no range, or it turns on facts you lack: age, sex, medication, assay.Use
valueinstead. - You are showing change over time, not position.Use
trend-sparklineinstead. - The number is a score you calculated, not a measurement.Use
score-dialinstead. - Only the clinical status level matters.Use
status-pillinstead. - You want to say what to do about it.Use
care-cardinstead.
Clinical meaning
Asserts. This measurement, in this unit, at this position against this range, from this source. Nothing about cause or trend.
Never read as. A diagnosis or verdict. Outside the range is not "abnormal", inside is not "normal": Reference ranges has the banned words.
Colour axis. Status only, when the product supplies one. category tints the label
alone; position never becomes a status.
The two colour axes.
Thresholds. The product owns every range, status and
precision. No built-in table, no
staleAfterHours. Staleness comes from RelativeTime.
Anatomy
RangeBarThe root. Colour from neither axis.RangeBar.LabelWhat was measured, the only category-tinted part.Controlled bycategoryStatusPillThe level the product assigned.Controlled bystatusRangeBar.TrackThe drawn extent, aria-hidden, never a slider.Controlled byrangeRangeBar.BandThe range itself, outlined, never axis-coloured.RangeBar.MarkA boundary and its number, at the metric's precision.Controlled byprecisionRangeBar.TickThe reading's position, the only status-coloured part.Controlled byvalueRangeBar.ValueThe reading and its unit, for the eye alone.Controlled byunitRangeBar.SummaryReading, position, range and source. Rewordable, never removable.Controlled bysummaryRangeBar.FootnoteWhen the reading was taken, or that nobody knows.Controlled bymeasuredAt
- RangeBar 1
- RangeBar.Label 1
- StatusPill 0..1
- RangeBar.Track 0..1
- RangeBar.Band 1
- RangeBar.Mark 2
- RangeBar.Tick 1
- RangeBar.Value 1
- RangeBar.Summary 1
- RangeBar.Footnote 0..1
Examples
Beyond the top of the range
The tick sits outside the band, and that is not a verdict.
range-bar-outside-the-range · base base · style base-lyraOpen under the product themeOne bound only
One bound has no width, so no track is drawn. The summary names the direction.
range-bar-an-open-ended-range · base base · style base-lyraOpen under the product themeThree readings down a page
category on the labels, status on the ticks, no element taking both.
range-bar-in-a-list-of-results · base base · style base-lyraOpen under the product themeFar above the range
The extent stretches to contain value, so a far reading squeezes the band.
range-bar-far-above-the-range · base base · style base-lyraOpen under the product themeStates
| Data state | What RangeBar renders |
|---|---|
| Error | It owns no fetch. A range with no source, neither bound, or downward bounds is discarded and reported. |
| Loading | No skeleton or spinner; renders synchronously. |
| Empty | value of null differs from 0: no tick, and the summary names the range. |
| Partial | No range, or one bound, draws no track. The summary carries the comparison. |
| Stale | It states when the reading was taken, or that nobody knows. No boundary. |
Content guidelines
The label is what was measured, in the reader's words. The summary states position, never a verdict.
Do
"26 mg/dL, above the range 10 to 20 mg/dL, from Example data."
Don’t
"26 mg/dL. Abnormal. Your result is poor." A verdict with no basis.
Accessibility
Audited against WCAG 2.2 AA over the source and the rendered preview. The audit is author-run, not independent, and clinical review is pending. The rendered pass was clean: no axe violations, no focus-visible failure, no horizontal scroll at 320px. It is display-only, so nothing is focusable and the keyboard and target-size checks do not apply.
- The summary paragraph is unconditional: reading, range with its source, and within, above or below in words, so the picture is never named.
RangeBar.Trackisaria-hidden, with noslider,meterorprogressbarrole.- Numbers go through Value, so the unit is spoken, not spelled.
- Status carries a word and a glyph through StatusPill, redundant to the tick colour.
- Under
forced-colorsthe tick keeps aHighlightfill andCanvasTextoutline.
Residual. The band outline and the two boundary marks fall below the 3:1 non-text
contrast floor. They stay because the graphic is aria-hidden and the summary carries
the same facts. Its own text and colour pairs are not measured yet, so the report
below stands in.
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| activity ink on activity surface | light | 88.5 | 9.61:1 | Pass |
| activity ink on activity surface | dark | -83.1 | 12.58:1 | Pass |
| activity line on activity surface | light | 67.6 | 4.42:1 | Pass |
| activity line on activity surface | dark | -69.0 | 9.93:1 | Pass |
| activity accent on activity surface | light | 44.1 | 2.34:1 | Below floor |
| activity accent on activity surface | dark | -53.3 | 7.26:1 | Pass |
| activity accent on the page | light | 46.1 | 2.41:1 | Below floor |
| activity accent on the page | dark | -54.6 | 8.22:1 | Pass |
| activity ink on the page | light | 90.5 | 9.90:1 | Pass |
| activity ink on the page | dark | -84.4 | 14.24:1 | Pass |
| activity line on the band | light | 66.1 | 4.32:1 | Pass |
| activity line on the band | dark | -67.4 | 8.60:1 | Pass |
| heart ink on heart surface | light | 89.1 | 10.51:1 | Pass |
| heart ink on heart surface | dark | -80.5 | 12.55:1 | Pass |
| heart line on heart surface | light | 70.4 | 5.05:1 | Pass |
| heart line on heart surface | dark | -64.5 | 9.46:1 | Pass |
| heart accent on heart surface | light | 60.3 | 3.64:1 | Pass |
| heart accent on heart surface | dark | -48.2 | 6.68:1 | Pass |
| heart accent on the page | light | 63.2 | 3.81:1 | Pass |
| heart accent on the page | dark | -49.5 | 7.27:1 | Pass |
| heart ink on the page | light | 92.0 | 10.97:1 | Pass |
| heart ink on the page | dark | -81.8 | 13.65:1 | Pass |
| heart line on the band | light | 69.9 | 5.01:1 | Pass |
| heart line on the band | dark | -62.8 | 7.88:1 | Pass |
| labs ink on labs surface | light | 88.8 | 9.78:1 | Pass |
| labs ink on labs surface | dark | -83.1 | 12.69:1 | Pass |
| labs line on labs surface | light | 67.9 | 4.51:1 | Pass |
| labs line on labs surface | dark | -68.4 | 9.90:1 | Pass |
| labs accent on labs surface | light | 59.3 | 3.49:1 | Pass |
| labs accent on labs surface | dark | -52.7 | 7.21:1 | Pass |
| labs accent on the page | light | 61.5 | 3.60:1 | Pass |
| labs accent on the page | dark | -53.9 | 8.07:1 | Pass |
| labs ink on the page | light | 90.9 | 10.09:1 | Pass |
| labs ink on the page | dark | -84.3 | 14.22:1 | Pass |
| labs line on the band | light | 66.5 | 4.42:1 | Pass |
| labs line on the band | dark | -66.7 | 8.49:1 | Pass |
| mind ink on mind surface | light | 89.9 | 10.36:1 | Pass |
| mind ink on mind surface | dark | -81.3 | 12.56:1 | Pass |
| mind line on mind surface | light | 70.7 | 4.94:1 | Pass |
| mind line on mind surface | dark | -65.4 | 9.54:1 | Pass |
| mind accent on mind surface | light | 60.0 | 3.55:1 | Pass |
| mind accent on mind surface | dark | -49.1 | 6.77:1 | Pass |
| mind accent on the page | light | 62.8 | 3.70:1 | Pass |
| mind accent on the page | dark | -50.0 | 7.42:1 | Pass |
| mind ink on the page | light | 92.7 | 10.80:1 | Pass |
| mind ink on the page | dark | -82.2 | 13.77:1 | Pass |
| mind line on the band | light | 70.0 | 4.89:1 | Pass |
| mind line on the band | dark | -63.4 | 8.00:1 | Pass |
| nutrition ink on nutrition surface | light | 89.3 | 10.06:1 | Pass |
| nutrition ink on nutrition surface | dark | -81.9 | 12.55:1 | Pass |
| nutrition line on nutrition surface | light | 69.7 | 4.78:1 | Pass |
| nutrition line on nutrition surface | dark | -66.7 | 9.66:1 | Pass |
| nutrition accent on nutrition surface | light | 39.2 | 2.11:1 | Below floor |
| nutrition accent on nutrition surface | dark | -50.2 | 6.88:1 | Pass |
| nutrition accent on the page | light | 41.8 | 2.19:1 | Below floor |
| nutrition accent on the page | dark | -51.3 | 7.63:1 | Pass |
| nutrition ink on the page | light | 91.9 | 10.44:1 | Pass |
| nutrition ink on the page | dark | -82.9 | 13.91:1 | Pass |
| nutrition line on the band | light | 68.8 | 4.71:1 | Pass |
| nutrition line on the band | dark | -64.8 | 8.19:1 | Pass |
| sleep ink on sleep surface | light | 89.6 | 10.19:1 | Pass |
| sleep ink on sleep surface | dark | -81.9 | 12.60:1 | Pass |
| sleep line on sleep surface | light | 70.0 | 4.80:1 | Pass |
| sleep line on sleep surface | dark | -66.4 | 9.65:1 | Pass |
| sleep accent on sleep surface | light | 64.6 | 4.05:1 | Pass |
| sleep accent on sleep surface | dark | -50.4 | 6.94:1 | Pass |
| sleep accent on the page | light | 67.3 | 4.22:1 | Pass |
| sleep accent on the page | dark | -51.4 | 7.67:1 | Pass |
| sleep ink on the page | light | 92.3 | 10.60:1 | Pass |
| sleep ink on the page | dark | -82.9 | 13.92:1 | Pass |
| sleep line on the band | light | 69.1 | 4.75:1 | Pass |
| sleep line on the band | dark | -64.5 | 8.16:1 | Pass |
| attention ink on attention surface | light | 89.4 | 10.35:1 | Pass |
| attention ink on attention surface | dark | -81.1 | 12.54:1 | Pass |
| attention line on attention surface | light | 70.0 | 4.90:1 | Pass |
| attention line on attention surface | dark | -65.5 | 9.54:1 | Pass |
| attention accent on attention surface | light | 65.1 | 4.18:1 | Pass |
| attention accent on attention surface | dark | -49.4 | 6.80:1 | Pass |
| attention ink on the attention band | light | 22.4 | 2.48:1 | Below floor |
| attention ink on the attention band | dark | -31.0 | 1.84:1 | Below floor |
| attention accent on the page | light | 67.8 | 4.35:1 | Pass |
| attention accent on the page | dark | -50.5 | 7.47:1 | Pass |
| attention ink on the page | light | 92.2 | 10.77:1 | Pass |
| attention ink on the page | dark | -82.3 | 13.76:1 | Pass |
| steady ink on steady surface | light | 88.3 | 9.62:1 | Pass |
| steady ink on steady surface | dark | -82.9 | 12.56:1 | Pass |
| steady line on steady surface | light | 67.5 | 4.45:1 | Pass |
| steady line on steady surface | dark | -68.9 | 9.91:1 | Pass |
| steady accent on steady surface | light | 56.4 | 3.21:1 | Pass |
| steady accent on steady surface | dark | -53.2 | 7.25:1 | Pass |
| steady ink on the steady band | light | 30.0 | 3.00:1 | Below floor |
| steady ink on the steady band | dark | -28.8 | 1.73:1 | Below floor |
| steady accent on the page | light | 58.6 | 3.31:1 | Pass |
| steady accent on the page | dark | -54.5 | 8.19:1 | Pass |
| steady ink on the page | light | 90.6 | 9.94:1 | Pass |
| steady ink on the page | dark | -84.2 | 14.20:1 | Pass |
| unknown ink on unknown surface | light | 89.5 | 9.98:1 | Pass |
| unknown ink on unknown surface | dark | -82.2 | 12.56:1 | Pass |
| unknown line on unknown surface | light | 69.2 | 4.64:1 | Pass |
| unknown line on unknown surface | dark | -67.2 | 9.72:1 | Pass |
| unknown accent on unknown surface | light | 43.1 | 2.29:1 | Below floor |
| unknown accent on unknown surface | dark | -51.3 | 7.05:1 | Pass |
| unknown accent on the page | light | 45.6 | 2.37:1 | Below floor |
| unknown accent on the page | dark | -52.3 | 7.84:1 | Pass |
| unknown ink on the page | light | 92.0 | 10.34:1 | Pass |
| unknown ink on the page | dark | -83.1 | 13.97:1 | Pass |
| urgent ink on urgent surface | light | 88.2 | 10.58:1 | Pass |
| urgent ink on urgent surface | dark | -80.3 | 12.52:1 | Pass |
| urgent line on urgent surface | light | 70.0 | 5.09:1 | Pass |
| urgent line on urgent surface | dark | -64.8 | 9.52:1 | Pass |
| urgent accent on urgent surface | light | 72.3 | 5.58:1 | Pass |
| urgent accent on urgent surface | dark | -48.0 | 6.63:1 | Pass |
| urgent ink on the urgent band | light | 13.9 | 1.90:1 | Below floor |
| urgent ink on the urgent band | dark | -31.5 | 1.89:1 | Below floor |
| urgent accent on the page | light | 75.3 | 5.84:1 | Pass |
| urgent accent on the page | dark | -49.5 | 7.24:1 | Pass |
| urgent ink on the page | light | 91.2 | 11.06:1 | Pass |
| urgent ink on the page | dark | -81.8 | 13.66:1 | Pass |
| watch ink on watch surface | light | 89.4 | 10.02:1 | Pass |
| watch ink on watch surface | dark | -82.1 | 12.58:1 | Pass |
| watch line on watch surface | light | 69.5 | 4.72:1 | Pass |
| watch line on watch surface | dark | -66.8 | 9.68:1 | Pass |
| watch accent on watch surface | light | 61.7 | 3.72:1 | Pass |
| watch accent on watch surface | dark | -50.7 | 6.95:1 | Pass |
| watch ink on the watch band | light | 25.7 | 2.70:1 | Below floor |
| watch ink on the watch band | dark | -30.6 | 1.81:1 | Below floor |
| watch accent on the page | light | 64.2 | 3.85:1 | Pass |
| watch accent on the page | dark | -51.7 | 7.71:1 | Pass |
| watch ink on the page | light | 91.9 | 10.38:1 | Pass |
| watch ink on the page | dark | -83.1 | 13.95:1 | Pass |
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
| activity ink on the card material | light | 93.6 | 10.34:1 | Pass |
| activity ink on the card material | dark | -83.5 | 12.91:1 | Pass |
| heart ink on the card material | light | 95.1 | 11.47:1 | Pass |
| heart ink on the card material | dark | -80.9 | 12.38:1 | Pass |
| labs ink on the card material | light | 94.0 | 10.54:1 | Pass |
| labs ink on the card material | dark | -83.5 | 12.90:1 | Pass |
| mind ink on the card material | light | 95.7 | 11.28:1 | Pass |
| mind ink on the card material | dark | -81.4 | 12.49:1 | Pass |
| nutrition ink on the card material | light | 95.0 | 10.91:1 | Pass |
| nutrition ink on the card material | dark | -82.0 | 12.61:1 | Pass |
| sleep ink on the card material | light | 95.4 | 11.08:1 | Pass |
| sleep ink on the card material | dark | -82.1 | 12.62:1 | Pass |
| attention surface on the card material | light | 0.0 | 1.09:1 | Below floor |
| attention surface on the card material | dark | 0.0 | 1.00:1 | Below floor |
| steady surface on the card material | light | 0.0 | 1.08:1 | Below floor |
| steady surface on the card material | dark | 0.0 | 1.03:1 | Below floor |
| urgent surface on the card material | light | 0.0 | 1.09:1 | Below floor |
| urgent surface on the card material | dark | 0.0 | 1.01:1 | Below floor |
| watch surface on the card material | light | 0.0 | 1.08:1 | Below floor |
| watch surface on the card material | dark | 0.0 | 1.01:1 | Below floor |
| the focus ring on the attention surface | light | 67.3 | 4.44:1 | Pass |
| the focus ring on the attention surface | dark | -45.9 | 6.30:1 | Pass |
| attention ink on the neutral hover fill | light | 88.6 | 10.23:1 | Pass |
| attention ink on the neutral hover fill | dark | -79.3 | 10.53:1 | Pass |
| the focus ring on the steady surface | light | 67.8 | 4.47:1 | Pass |
| the focus ring on the steady surface | dark | -45.8 | 6.12:1 | Pass |
| steady ink on the neutral hover fill | light | 87.0 | 9.43:1 | Pass |
| steady ink on the neutral hover fill | dark | -81.3 | 10.86:1 | Pass |
| the focus ring on the urgent surface | light | 67.0 | 4.42:1 | Pass |
| the focus ring on the urgent surface | dark | -45.5 | 6.34:1 | Pass |
| urgent ink on the neutral hover fill | light | 87.7 | 10.50:1 | Pass |
| urgent ink on the neutral hover fill | dark | -78.9 | 10.46:1 | Pass |
| the focus ring on the watch surface | light | 67.6 | 4.46:1 | Pass |
| the focus ring on the watch surface | dark | -46.0 | 6.24:1 | Pass |
| watch ink on the neutral hover fill | light | 88.3 | 9.86:1 | Pass |
| watch ink on the neutral hover fill | dark | -80.1 | 10.67:1 | Pass |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
Data attributes
| Attribute | Condition | Value |
|---|---|---|
| data-slot | On every part, always | range-bar, range-bar-label, status-pill, range-bar-track, range-bar-band, range-bar-mark, range-bar-tick, range-bar-value, range-bar-summary, range-bar-footnote |
| data-status | On the StatusPill and the tick, when a status was supplied | steady | watch | attention | urgent |
| data-category | On the label, when a category was supplied | sleep | heart | activity | nutrition | mind | labs |
| data-opsinjs-value | On every Value, six per drawn bar | The unrounded number, or empty with no reading |
CSS variables
| Selector | Variable | Controls |
|---|---|---|
| div:has(> [data-slot="range-bar-track"]) | --rb-label-half | How far the reading label's midpoint is clamped from either track edge. Set inline at 3.5em. |
API reference
Prop
Type
Generated from RangeBarProps in registry/bases/base/range-bar.tsx.
precision is decimal places, not significant figures, and it is required. A status
outside the four levels is dropped, never approximated. Omit locale and the server
formats with its own while the browser uses the reader's, so the two disagree.
Related
- ScoreDial, a calculated score against its bands.
- TrendSparkline, change over time.
- Progress, progress towards a goal.
- StatusPill, the level alone.
- MetricTile, the compact dashboard form.