opsinjs
ComponentsHealth data display

RangeLegend

The key that names the tones a RangeBar draws, so a reader learns in one place what the reference band and each status colour mean. It asserts nothing itself.

Preview

Theme
Density
Text
Status
range-legend · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/range-legend

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 { RangeLegend } from "@/components/ui/range-legend"
<RangeLegend
  bands={[
    { label: "The usual range", description: "From your laboratory. Most results sit here." },
    { label: "Worth watching", tone: "watch" },
    { label: "Needs attention", tone: "attention" },
  ]}
/>

When to use it

Use it when

  • Beside RangeBars that share a status vocabulary, naming each level once instead of a pill per row.
  • As the key beneath a single RangeBar whose status tones a first time reader has not met.
  • Wherever a reader must learn the neutral band and the status colours before reading the numbers.

Do not use it when

  • You are showing one measurement against its range; the bar already states reading, range and position.Use range-bar instead.
  • You want the level of one reading. A legend names the vocabulary, never a verdict.Use status-pill instead.
  • You are explaining a single clinical term rather than a set of tones.Use term instead.

Clinical meaning

Asserts. The tones the bar beside it draws, and what each one means. Nothing about any reading, cause or next step.

Never read as. A scale, a diagnosis or a verdict on the reader. "Needs attention" names a tone a product may assign, not the reader's result. "Normal" and "abnormal" are banned; Reference ranges and normal carries the list.

Colour axis. The status axis and the neutral reference range band only. No element carries data-category, because The two colour axes never mix. Every status row draws fill, glyph and word together, so the level survives greyscale.

Thresholds. The product owns every word, range and level. RangeLegend ships no band names; words arrive as props and the swatch colours are all it supplies.

Anatomy

  1. RangeLegendThe root list. No axis colour, no data.Controlled by bands
  2. RangeLegend.ItemOne row. data-status on a status row, none on the neutral band row.Controlled by tone
  3. RangeLegend.SwatchThe colour chip in the bar's colour. Outlined for the neutral band, filled for a status.
  4. RangeLegend.LabelThe row's word, the product's phrasing. The only part a screen reader announces.Controlled by label
  5. RangeLegend.GlyphThe level's distinct lucide shape, status rows only, in neutral ink. Decorative; the word is the level.
  • RangeLegend 1data-slot="range-legend", a ul.
    • RangeLegend.Item 1..ndata-slot="range-legend-item", a li. data-status="<level>" on a status row only.
      • RangeLegend.Swatch 1data-slot="range-legend-swatch", aria-hidden="true".
      • RangeLegend.Glyph 0..1A lucide glyph, aria-hidden="true". Status rows only, no data-slot.
      • RangeLegend.Label 1data-slot="range-legend-label". The description is a plain span, no slot.

Examples

The legend beside its bar

The same container as the bars it names, bands holding the tones they draw. The swatches match the ticks above.

Theme
Density
Text
Status
range-legend-beside-its-bar · base base · style base-lyraOpen under the product theme

The four levels, named once

The whole vocabulary in one key: the neutral band and four status levels, each row's tone giving its word, glyph and colour.

Theme
Density
Text
Status
range-legend-the-four-levels · base base · style base-lyraOpen under the product theme

Content guidelines

Write each label in the reader's plain words, not a clinical register. The description is a short second line saying who set the range or what the tone means, and the neutral band's word stays about the range, not the reader.

Do

"The usual range" with a neutral swatch, then "Worth watching" with its word, glyph and colour.

Don’t

"Normal" with a green swatch and "Abnormal" with a red one. A verdict on the person, carried by colour alone.

Accessibility

Audited against WCAG 2.2 AA in a source and a rendered pass. The audit is author-run, not independent, and clinical review is pending.

What the audit fixed.

  • The legend <ul> had no accessible name. Optional aria-label and aria-labelledby props forward onto the root, so a caller names the key or points it at the bar's heading.
  • A blank status label would draw colour and glyph with no word, a colour-alone failure. A guard warns on it, with the empty-bands and invalid-tone guards.

What still holds from the source.

  • Each swatch and glyph carries aria-hidden, so a screen reader skips the decoration.
  • No element carries both data-category and data-status, and this file writes no category colour.
  • Nothing takes focus, answers a key or animates, so prefers-reduced-motion has no state to reduce.
  • At 200% text the swatches hold size, the words wrap, and no ellipsis is set.
  • Under forced-colors: active a status fill is stripped and the swatch keeps a CanvasText outline.

Residual and deferred.

  • Status swatches are fills of RangeBar's tick tones with no explicit border, so swatch-to-page contrast is unmeasured. Each swatch is aria-hidden decoration redundant to the word and glyph, so a low-contrast swatch loses nothing. Measuring the four fills and the neutral border in light and dark is a recommended enhancement.
  • Label and description use the foreground and muted-foreground role tokens, not re-measured in this pass.
  • The legend cannot enforce in code that it sits beside its bar; aria-labelledby is the mechanism a caller uses to bind it, and the audit cannot verify callers do.
KeyActionNotes
TabSkips the whole componentNothing here is focusable.
Shift+TabSkips the whole componentNever in the focus order.
Arrow keysNothingSwatches are decorative, words static.
PairThemeAPCA LcWCAG 2.2Floor
attention ink on attention surfacelight89.410.35:1Pass
attention ink on attention surfacedark-81.112.54:1Pass
attention line on attention surfacelight70.04.90:1Pass
attention line on attention surfacedark-65.59.54:1Pass
attention accent on attention surfacelight65.14.18:1Pass
attention accent on attention surfacedark-49.46.80:1Pass
attention ink on the attention bandlight22.42.48:1Below floor
attention ink on the attention banddark-31.01.84:1Below floor
attention accent on the pagelight67.84.35:1Pass
attention accent on the pagedark-50.57.47:1Pass
attention ink on the pagelight92.210.77:1Pass
attention ink on the pagedark-82.313.76:1Pass
steady ink on steady surfacelight88.39.62:1Pass
steady ink on steady surfacedark-82.912.56:1Pass
steady line on steady surfacelight67.54.45:1Pass
steady line on steady surfacedark-68.99.91:1Pass
steady accent on steady surfacelight56.43.21:1Pass
steady accent on steady surfacedark-53.27.25:1Pass
steady ink on the steady bandlight30.03.00:1Below floor
steady ink on the steady banddark-28.81.73:1Below floor
steady accent on the pagelight58.63.31:1Pass
steady accent on the pagedark-54.58.19:1Pass
steady ink on the pagelight90.69.94:1Pass
steady ink on the pagedark-84.214.20:1Pass
unknown ink on unknown surfacelight89.59.98:1Pass
unknown ink on unknown surfacedark-82.212.56:1Pass
unknown line on unknown surfacelight69.24.64:1Pass
unknown line on unknown surfacedark-67.29.72:1Pass
unknown accent on unknown surfacelight43.12.29:1Below floor
unknown accent on unknown surfacedark-51.37.05:1Pass
unknown accent on the pagelight45.62.37:1Below floor
unknown accent on the pagedark-52.37.84:1Pass
unknown ink on the pagelight92.010.34:1Pass
unknown ink on the pagedark-83.113.97:1Pass
urgent ink on urgent surfacelight88.210.58:1Pass
urgent ink on urgent surfacedark-80.312.52:1Pass
urgent line on urgent surfacelight70.05.09:1Pass
urgent line on urgent surfacedark-64.89.52:1Pass
urgent accent on urgent surfacelight72.35.58:1Pass
urgent accent on urgent surfacedark-48.06.63:1Pass
urgent ink on the urgent bandlight13.91.90:1Below floor
urgent ink on the urgent banddark-31.51.89:1Below floor
urgent accent on the pagelight75.35.84:1Pass
urgent accent on the pagedark-49.57.24:1Pass
urgent ink on the pagelight91.211.06:1Pass
urgent ink on the pagedark-81.813.66:1Pass
watch ink on watch surfacelight89.410.02:1Pass
watch ink on watch surfacedark-82.112.58:1Pass
watch line on watch surfacelight69.54.72:1Pass
watch line on watch surfacedark-66.89.68:1Pass
watch accent on watch surfacelight61.73.72:1Pass
watch accent on watch surfacedark-50.76.95:1Pass
watch ink on the watch bandlight25.72.70:1Below floor
watch ink on the watch banddark-30.61.81:1Below floor
watch accent on the pagelight64.23.85:1Pass
watch accent on the pagedark-51.77.71:1Pass
watch ink on the pagelight91.910.38:1Pass
watch ink on the pagedark-83.113.95:1Pass
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor
attention surface on the card materiallight0.01.09:1Below floor
attention surface on the card materialdark0.01.00:1Below floor
steady surface on the card materiallight0.01.08:1Below floor
steady surface on the card materialdark0.01.03:1Below floor
urgent surface on the card materiallight0.01.09:1Below floor
urgent surface on the card materialdark0.01.01:1Below floor
watch surface on the card materiallight0.01.08:1Below floor
watch surface on the card materialdark0.01.01:1Below floor
the focus ring on the attention surfacelight67.34.44:1Pass
the focus ring on the attention surfacedark-45.96.30:1Pass
attention ink on the neutral hover filllight88.610.23:1Pass
attention ink on the neutral hover filldark-79.310.53:1Pass
the focus ring on the steady surfacelight67.84.47:1Pass
the focus ring on the steady surfacedark-45.86.12:1Pass
steady ink on the neutral hover filllight87.09.43:1Pass
steady ink on the neutral hover filldark-81.310.86:1Pass
the focus ring on the urgent surfacelight67.04.42:1Pass
the focus ring on the urgent surfacedark-45.56.34:1Pass
urgent ink on the neutral hover filllight87.710.50:1Pass
urgent ink on the neutral hover filldark-78.910.46:1Pass
the focus ring on the watch surfacelight67.64.46:1Pass
the focus ring on the watch surfacedark-46.06.24:1Pass
watch ink on the neutral hover filllight88.39.86:1Pass
watch ink on the neutral hover filldark-80.110.67:1Pass

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

Data attributes

AttributeConditionValue
data-slotOn the root and every named inner part, alwaysrange-legend, range-legend-item, range-legend-swatch, range-legend-label
data-statusOn a status row's item; absent on the neutral band rowThe row's tone: steady, watch, attention or urgent

API reference

Prop

Type

Generated from RangeLegendProps in registry/bases/base/range-legend.tsx.

tone is typed to the four ClinicalStatus levels in Clinical status semantics. A value outside the four is refused with a development warning and falls back to the neutral band. An empty bands array renders nothing and warns in development.

  • RangeBar renders the bar this legend explains, with its own summary sentence and status pill.
  • StatusPill names the level of one reading; RangeLegend names the whole vocabulary a group of bars shares.
  • Term explains one clinical word in place, where a legend names a set of tones.

On this page