opsinjs
ComponentsHealth formatting

Value

A number and its unit, formatted the same way everywhere. It is rounded honestly, spoken correctly, and never shown to more precision than the measurement has.

Preview

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

Installation

pnpm dlx shadcn@latest add @opsinjs/value

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 { Value } from "@/components/ui/value"
<Value value={1000.2} unit="kg" precision={1} />

When to use it

Use it when

  • Any number displayed to a reader that has a unit, a precision, or both.
  • Inside other components: the reading in a MetricTile, the number beside a RangeBar tick, the score in a ScoreDial.
  • Anywhere the same measurement appears on more than one screen and must look identical on all of them.

Do not use it when

  • The number needs its reference range shown alongside it to mean anything.Use range-bar instead.
  • The number is a point in time rather than a quantity.Use relative-time instead.
  • You are collecting the number rather than displaying it.Use reading-input instead.
  • The number needs a label, a timestamp and a surface of its own.Use metric-tile instead.
  • The number is a proportion or a risk that needs a denominator and a natural-frequency framing.Use callout instead.

Clinical meaning

Asserts. That this quantity has this magnitude in this unit, to this precision. 1000.2 and 1000.20 claim different things.

Never read as. A judgement. Value carries no status and no band; a bare number means nothing without the context its surface supplies.

Colour axis. Neither. A reading inherits the text colour of its context. An absence at display takes the muted theme role.

Thresholds. None here, and none in tokens/units.json, which holds no reference range, no plausibility bound and no default precision. Precision belongs to the metric, so precision is required. See Numbers, units and precision and Reference ranges.

Masking. On-screen privacy lists this among the components that implement a fixed-width mask. Value has no masked prop and reserves no width, so masking a reading today moves the layout.

Anatomy

  1. ValueThe root, and one accessible unit of meaning. Not focusable, not interactive, and it paints no colour for a reading.Controlled by value
  2. Value.NumberThe formatted magnitude in tabular figures, rounded once at display, half away from zero, with locale-aware separators.Controlled by precision
  3. Value.UnitThe symbol as written, carrying the no-break space that joins it to the number inside this part.Controlled by unit
  4. Value.SpokenThe unit in words, visually hidden and select-none. Present only when the reader's language is English or unset.Controlled by unit
  5. Value.AbsenceWords and no glyph: no reading yet for one never taken, not available for one that arrived broken.Controlled by absenceLabel

Examples

Zero is a reading. Absence is not.

Three rows share a unit and a precision and differ only in the reading: a number, a null, and a number that arrived broken. Rule 13 keeps them apart.

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

One precision makes a column comparable

Four readings of one metric at one precision, aligned by the layout and by tabular figures.

Theme
Density
Text
Status
value-aligned-in-a-column · base base · style base-lyraOpen under the product theme

What a screen reader is actually given

One magnitude with six units, and beside each the phrase that reaches speech, which a screenshot cannot show.

Theme
Density
Text
Status
value-spoken-units · base base · style base-lyraOpen under the product theme

Content guidelines

A caller's absenceLabel is rendered exactly as passed, with no check, so run your own absence copy past the banned-word list. Write a unit symbol exactly as tokens/units.json spells it, case included. A number with no unit by design takes null, never an omitted prop.

Do

"1000.2 kg" for a device that measures to 100 g. One decimal place, spoken as 1000.2 kilograms.

Don’t

"1000.2481 kg" because that is what the device sent. Four decimal places assert a precision the instrument lacks.

Accessibility

Audited against WCAG 2.2 AA in a source pass and a rendered pass on /view/base/base-lyra/component/value. The audit is author-run, not an independent review, and clinical review is still pending.

It changed nothing in source. The unit symbol is aria-hidden only when a spoken form exists and the words follow it, so an English reader hears 1000.2 kilograms as one phrase. Absence is spoken in words, never drawn at hero weight, and carries no decorative glyph. The component imposes no truncation, no fixed height and no whitespace-nowrap. The rendered pass returned empty axe, target-size, focus-visible and 320px reflow results.

Three items sit outside this source. A Value must never be the sole accessible name of a tappable control, because the visible symbol and the spoken words share no substring, so the control owes its own name carrying the symbol (WCAG 2.2 SC 2.5.3). The only path to a clipped reading is a caller passing a truncating class through className, which twMerge lets win, so truncation is a caller responsibility documented on ValueProps.className. The spoken span uses select-none to keep the words off the clipboard while leaving them in the accessibility tree, untested by a copy test.

KeyActionNotes
TabSkips the valueNot focusable, by contract. Thirty readings would otherwise be thirty tab stops doing nothing.
Shift+TabSkips the valueSame in reverse. A tappable row owes its own name and its own 44 by 44.
PairThemeAPCA LcWCAG 2.2Floor
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

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 inner part, when that part rendersvalue, value-number, value-unit, value-spoken, value-absence
data-opsinjs-valueOn the root, alwaysThe unrounded reading, or an empty string when there is no reading

API reference

Prop

Type

Generated from ValueProps in registry/bases/base/value.tsx.

precision is required and nothing is defaulted in its place. unit={null} declares a dimensionless number. Omitting unit raises OPSIN-0003 instead. Under server rendering, pass locale explicitly, or the server's own ICU default formats the first paint and hydration reformats it.

  • RelativeTime does the same job for points in time, and shares the absence contract.
  • ReadingInput is the entry counterpart, so the two must agree on precision and units.
  • MetricTile is a Value with a label, a time and a surface of its own.
  • RangeBar is a Value with its reference range drawn around it.
  • Callout owns the framing a bare risk proportion needs before it can be shown.

On this page