opsinjs
ComponentsHealth data display

ScoreDial

A single composite score shown on a dial with its band named in words, alongside an honest statement of what went into the number.

Preview

Theme
Density
Text
Status
score-dial · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/score-dial

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 { ScoreDial } from "@/components/ui/score-dial"
<ScoreDial
  label="Example composite score"
  value={14}
  min={10}
  max={20}
  precision={0}
  bands={[]}
  derivation="Example data rather than a reference range. The score and the scale here are invented."
  calculatedAt="2026-03-14T08:12:00+00:00"
/>

When to use it

Use it when

  • A questionnaire total, a readiness index, or any score you calculated, meaningful only against its own bands.
  • A bounded score whose scale the reader has no intuition for, where position carries more than digits.
  • A score whose derivation you can state in one sentence, on the same screen.

Do not use it when

  • The number is a measurement rather than a calculation, with a published reference range.Use range-bar instead.
  • You cannot say what went into the score, or it is a risk or probability, which a dial must never draw as an angle.Use value instead.
  • The score is only meaningful as a trajectory, so one number says nothing alone.Use trend-sparkline instead.
  • You are showing completion of something the person is doing, such as steps against a goal.Use progress instead.
  • The score crossed a threshold that needs action now. The dial refuses attention and urgent.Use care-card instead.

Clinical meaning

Asserts. The product computed a number from stated inputs, and it fell in a named band. It asserts a calculation, never a fact about the reader's body.

Never read as. A measurement, a diagnosis, a percentage or a rank. Risk and statistics keeps probabilities off it. The scale reads on a scale of 10 to 20, never out of 20.

Colour axis. Status sits on the indicator and the band the score fell in, only when the product passed status. Other bands stay neutral. Category tints the label alone, so the two colour axes never meet.

Thresholds. The product owns every band and cut-off; opsinjs ships none. ScoreBand.source is required, as Reference ranges demands. status is an input, never derived. attention and urgent are refused.

Staleness. calculatedAt renders as a plain date, with no staleness treatment. Add a RelativeTime when an old score must read as old.

Anatomy

  1. ScoreDialThe root. A stacked column, no colour, no tab stop.Controlled by className
  2. ScoreDial.LabelWhat the score is called. The only part category tints.Controlled by category
  3. ScoreDial.TrackThe SVG, role="img". Never a meter, a progressbar or a slider.Controlled by min, max
  4. ScoreDial.BandOne arc per band, neutral unless the score fell there with a status.Controlled by bands
  5. ScoreDial.BoundaryA radial mark at each internal band edge, so bands survive greyscale.
  6. ScoreDial.IndicatorThe score's position. Absent with no score, no width, or off-scale.Controlled by value
  7. ScoreDial.ReadingThe visible text twin: number, band name and StatusPill.Controlled by value, precision, locale, status
  8. ScoreDial.CoverageThe partial-data sentence, only when coverage is short.Controlled by coverage
  9. ScoreDial.ScaleThe bounds in words, each band with its range, and whose bands.Controlled by min, max, bands
  10. ScoreDial.DerivationWhat went into the score, over what window. Always shown.Controlled by derivation
  11. ScoreDial.CalculatedAtWhen the score was calculated. Without an instant it says nobody knows.Controlled by calculatedAt

Examples

opsinjs ships no bands

With an empty bands, the dial draws the scale, places the number and names no band.

Theme
Density
Text
Status
score-dial-no-bands-supplied · base base · style base-lyraOpen under the product theme

A score built from less than usual

coverage puts the gap on the dial's face. status="watch" is a prop on the dial, not a field on a band.

Theme
Density
Text
Status
score-dial-partial-data · base base · style base-lyraOpen under the product theme

Zero is a score; absence is not

value={0} reached the bottom of its scale. value={null} never ran, so nothing is placed.

Theme
Density
Text
Status
score-dial-zero-is-not-absence · base base · style base-lyraOpen under the product theme

Accessibility

Audited against WCAG 2.2 AA in a source pass and a rendered pass. The audit is author-run, not an independent review, and clinical review is still pending. It found nothing to change: no serious defect in the source, and a clean rendered pass over axe, target size and 320px reflow. There are no interactive elements, so keyboard and pointer criteria pass vacuously.

Argued from the source.

  • The graphic is role="img" with a complete aria-label and focusable="false", never a meter, progressbar or slider, which would assert a within-range value it may not make.
  • Every status tint carries a word and a glyph and stamps data-status, never colour alone.
  • The band name, scale, source and derivation are visible text no prop removes, the layout is one stacked column that never truncates at 200% text, and nothing animates.

Deferred. No live region mounts, because it is static and read-only, so nothing announces a score change; adding one is a recommended enhancement, not a fix. The tinted band strokes below the non-text floor in light, left as-is because the band name, the StatusPill word and glyph, the indicator and data-status all say the same thing.

Unmeasured. The pair the arc stroke forms against its card is not in the measured set; the source records it and types no number.

KeyActionNotes
TabSkips the dialA picture of a reading, not a control.
Shift+TabSkips the dialThe SVG carries focusable="false".
PairThemeAPCA LcWCAG 2.2Floor
activity ink on activity surfacelight88.59.61:1Pass
activity ink on activity surfacedark-83.112.58:1Pass
activity line on activity surfacelight67.64.42:1Pass
activity line on activity surfacedark-69.09.93:1Pass
activity accent on activity surfacelight44.12.34:1Below floor
activity accent on activity surfacedark-53.37.26:1Pass
activity accent on the pagelight46.12.41:1Below floor
activity accent on the pagedark-54.68.22:1Pass
activity ink on the pagelight90.59.90:1Pass
activity ink on the pagedark-84.414.24:1Pass
activity line on the bandlight66.14.32:1Pass
activity line on the banddark-67.48.60:1Pass
heart ink on heart surfacelight89.110.51:1Pass
heart ink on heart surfacedark-80.512.55:1Pass
heart line on heart surfacelight70.45.05:1Pass
heart line on heart surfacedark-64.59.46:1Pass
heart accent on heart surfacelight60.33.64:1Pass
heart accent on heart surfacedark-48.26.68:1Pass
heart accent on the pagelight63.23.81:1Pass
heart accent on the pagedark-49.57.27:1Pass
heart ink on the pagelight92.010.97:1Pass
heart ink on the pagedark-81.813.65:1Pass
heart line on the bandlight69.95.01:1Pass
heart line on the banddark-62.87.88:1Pass
labs ink on labs surfacelight88.89.78:1Pass
labs ink on labs surfacedark-83.112.69:1Pass
labs line on labs surfacelight67.94.51:1Pass
labs line on labs surfacedark-68.49.90:1Pass
labs accent on labs surfacelight59.33.49:1Pass
labs accent on labs surfacedark-52.77.21:1Pass
labs accent on the pagelight61.53.60:1Pass
labs accent on the pagedark-53.98.07:1Pass
labs ink on the pagelight90.910.09:1Pass
labs ink on the pagedark-84.314.22:1Pass
labs line on the bandlight66.54.42:1Pass
labs line on the banddark-66.78.49:1Pass
mind ink on mind surfacelight89.910.36:1Pass
mind ink on mind surfacedark-81.312.56:1Pass
mind line on mind surfacelight70.74.94:1Pass
mind line on mind surfacedark-65.49.54:1Pass
mind accent on mind surfacelight60.03.55:1Pass
mind accent on mind surfacedark-49.16.77:1Pass
mind accent on the pagelight62.83.70:1Pass
mind accent on the pagedark-50.07.42:1Pass
mind ink on the pagelight92.710.80:1Pass
mind ink on the pagedark-82.213.77:1Pass
mind line on the bandlight70.04.89:1Pass
mind line on the banddark-63.48.00:1Pass
nutrition ink on nutrition surfacelight89.310.06:1Pass
nutrition ink on nutrition surfacedark-81.912.55:1Pass
nutrition line on nutrition surfacelight69.74.78:1Pass
nutrition line on nutrition surfacedark-66.79.66:1Pass
nutrition accent on nutrition surfacelight39.22.11:1Below floor
nutrition accent on nutrition surfacedark-50.26.88:1Pass
nutrition accent on the pagelight41.82.19:1Below floor
nutrition accent on the pagedark-51.37.63:1Pass
nutrition ink on the pagelight91.910.44:1Pass
nutrition ink on the pagedark-82.913.91:1Pass
nutrition line on the bandlight68.84.71:1Pass
nutrition line on the banddark-64.88.19:1Pass
sleep ink on sleep surfacelight89.610.19:1Pass
sleep ink on sleep surfacedark-81.912.60:1Pass
sleep line on sleep surfacelight70.04.80:1Pass
sleep line on sleep surfacedark-66.49.65:1Pass
sleep accent on sleep surfacelight64.64.05:1Pass
sleep accent on sleep surfacedark-50.46.94:1Pass
sleep accent on the pagelight67.34.22:1Pass
sleep accent on the pagedark-51.47.67:1Pass
sleep ink on the pagelight92.310.60:1Pass
sleep ink on the pagedark-82.913.92:1Pass
sleep line on the bandlight69.14.75:1Pass
sleep line on the banddark-64.58.16:1Pass
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
activity ink on the card materiallight93.610.34:1Pass
activity ink on the card materialdark-83.512.91:1Pass
heart ink on the card materiallight95.111.47:1Pass
heart ink on the card materialdark-80.912.38:1Pass
labs ink on the card materiallight94.010.54:1Pass
labs ink on the card materialdark-83.512.90:1Pass
mind ink on the card materiallight95.711.28:1Pass
mind ink on the card materialdark-81.412.49:1Pass
nutrition ink on the card materiallight95.010.91:1Pass
nutrition ink on the card materialdark-82.012.61:1Pass
sleep ink on the card materiallight95.411.08:1Pass
sleep ink on the card materialdark-82.112.62:1Pass
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 every part that rendersscore-dial, score-dial-label, score-dial-track, score-dial-band, score-dial-boundary, score-dial-indicator, score-dial-reading, score-dial-score, score-dial-band-name, status-pill, score-dial-coverage, score-dial-scale, score-dial-band-source, score-dial-derivation, score-dial-calculated-line, score-dial-calculated-at
data-statusOn the band the score fell in, the indicator and the StatusPillsteady | watch
data-categoryOn ScoreDial.Label, with a categorysleep | heart | activity | nutrition | mind | labs
data-opsinjs-valueOn the Value inside ScoreDial.ScoreThe unrounded number, or empty with no reading

API reference

Prop

Type

Generated from ScoreDialProps in registry/bases/base/score-dial.tsx.

bands is required with no default, and an empty array is legitimate. A ScoreBand carries an inclusive from, an exclusive to, a name in the reader's language and a required source. status accepts only steady | watch. precision is required, as Numbers, units and precision demands. A non-finite value prints and speaks not available.

  • RangeBar is a measured value against a published range, not a calculation.
  • Progress is a task advancing towards a goal, not a score.
  • GoalRing is the ring the reader fills by doing something.
  • MetricTile is the compact readout, with no room for a derivation.
  • Value is honest when the derivation cannot be stated.

On this page