---
title: "Value"
description: "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."
url: "https://opsinjs.pensievelabs.org/components/value"
source: "https://opsinjs.pensievelabs.org/components/value.md"
section: "Components"
status: "shipped"
kind: "component"
category: "health-formatting"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["number", "unit", "format", "formatted number", "format a number", "numeric display", "figure"]
governedBy: ["numbers-units-precision", "unit-systems", "clinical-interaction-guidelines", "data-provenance-and-device-accuracy", "on-screen-privacy", "reference-ranges", "risk-and-statistics", "safety-review-checklist", "uncertainty-and-staleness", "who-this-is-for"]
usedIn: ["ask-users-for/height-and-weight", "choose-a-component", "daily-log-screen", "daily-logging", "diabetes-medicines-app", "forms/units-and-numeric-entry", "health-metric-card", "onboarding-screen", "results-screen", "sharing-with-a-clinician", "trend-review", "trends-screen", "value-against-a-range"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="value"
  status="shipped"
  questions="[
  &#x22;Whether a per-language spoken unit form belongs in tokens/units.json, in the product, or in neither, given that only an English reader hears words today.&#x22;,
  &#x22;Whether a hero value should move from title1 up to largeTitle, an answer that changes every hero number in MetricTile, ResultCard, ScoreDial and RangeBar at once.&#x22;,
  &#x22;Whether the muted absence at the display size clears contrast, and how the value behaves at 200% text, since neither has been looked at in either theme.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="value" />

## Installation [#installation]

<ComponentInstall name="value" unbuilt="false" importPath="@/components/ui/value" />

## Usage [#usage]

```tsx
import { Value } from "@/components/ui/value"
```

```tsx
<Value value={1000.2} unit="kg" precision={1} />
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;Any number displayed to a reader that has a unit, a precision, or both.&#x22;,
  &#x22;Inside other components: the reading in a MetricTile, the number beside a RangeBar tick, the score in a ScoreDial.&#x22;,
  &#x22;Anywhere the same measurement appears on more than one screen and must look identical on all of them.&#x22;,
]"
  avoid="[
  { case: &#x22;The number needs its reference range shown alongside it to mean anything.&#x22;, instead: &#x22;range-bar&#x22; },
  { case: &#x22;The number is a point in time rather than a quantity.&#x22;, instead: &#x22;relative-time&#x22; },
  { case: &#x22;You are collecting the number rather than displaying it.&#x22;, instead: &#x22;reading-input&#x22; },
  { case: &#x22;The number needs a label, a timestamp and a surface of its own.&#x22;, instead: &#x22;metric-tile&#x22; },
  { case: &#x22;The number is a proportion or a risk that needs a denominator and a natural-frequency framing.&#x22;, instead: &#x22;callout&#x22; },
]"
/>

## Clinical meaning [#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](../health/numbers-units-precision.mdx) and
[Reference ranges](../health/reference-ranges.mdx).

**Masking.** [On-screen privacy](../health/on-screen-privacy.mdx) 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.

<SafetyCallout severity="attention" evidence="opinion" title="The unit table says what a number is measured in. It never says what a number should be.">
  `mmol/L` to `mg/dL` is refused, because the factor is a property of the analyte
  rather than of either unit. `convertUnit` returns nothing rather than a
  substituted default. See [Unit systems](../health/unit-systems.mdx).
</SafetyCallout>

## Anatomy [#anatomy]

<Anatomy
  name="value"
  parts="[
  {
    name: &#x22;Value&#x22;,
    describes: &#x22;The root, and one accessible unit of meaning. Not focusable, not interactive, and it paints no colour for a reading.&#x22;,
    prop: &#x22;value&#x22;,
  },
  {
    name: &#x22;Value.Number&#x22;,
    describes: &#x22;The formatted magnitude in tabular figures, rounded once at display, half away from zero, with locale-aware separators.&#x22;,
    prop: &#x22;precision&#x22;,
  },
  {
    name: &#x22;Value.Unit&#x22;,
    describes: &#x22;The symbol as written, carrying the no-break space that joins it to the number inside this part.&#x22;,
    prop: &#x22;unit&#x22;,
  },
  {
    name: &#x22;Value.Spoken&#x22;,
    describes: &#x22;The unit in words, visually hidden and select-none. Present only when the reader's language is English or unset.&#x22;,
    prop: &#x22;unit&#x22;,
  },
  {
    name: &#x22;Value.Absence&#x22;,
    describes: &#x22;Words and no glyph: no reading yet for one never taken, not available for one that arrived broken.&#x22;,
    prop: &#x22;absenceLabel&#x22;,
  },
]"
/>

## Examples [#examples]

### Zero is a reading. Absence is not. [#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.

<ComponentPreview name="value-zero-is-not-absence" kind="example" align="start" />

### One precision makes a column comparable [#one-precision-makes-a-column-comparable]

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

<ComponentPreview name="value-aligned-in-a-column" kind="example" align="start" />

### What a screen reader is actually given [#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.

<ComponentPreview name="value-spoken-units" kind="example" align="start" />

## Content guidelines [#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.

<DoDont>
  <DoDont.Do>
    **"1000.2 kg"** for a device that measures to 100 g. One decimal place,
    spoken as *1000.2 kilograms*.
  </DoDont.Do>

  <DoDont.Dont>
    **"1000.2481 kg"** because that is what the device sent. Four decimal places
    assert a precision the instrument lacks.
  </DoDont.Dont>
</DoDont>

## Accessibility [#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.

<KeyboardTable
  name="value"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Skips the value&#x22;,
    notes: &#x22;Not focusable, by contract. Thirty readings would otherwise be thirty tab stops doing nothing.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Skips the value&#x22;,
    notes: &#x22;Same in reverse. A tappable row owes its own name and its own 44 by 44.&#x22;,
  },
]"
/>

<ContrastReport component="value" />

## Data attributes [#data-attributes]

<DataAttributesTable
  name="value"
  rows="[
  {
    attribute: &#x22;data-slot&#x22;,
    condition: &#x22;On the root and every inner part, when that part renders&#x22;,
    value: &#x22;value, value-number, value-unit, value-spoken, value-absence&#x22;,
  },
  {
    attribute: &#x22;data-opsinjs-value&#x22;,
    condition: &#x22;On the root, always&#x22;,
    value: &#x22;The unrounded reading, or an empty string when there is no reading&#x22;,
  },
]"
/>

## API reference [#api-reference]

<PropsTable name="ValueProps" />

`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.

## Related [#related]

* [RelativeTime](./relative-time.mdx) does the same job for points in time, and shares the absence contract.
* [ReadingInput](./reading-input.mdx) is the entry counterpart, so the two must agree on precision and units.
* [MetricTile](./metric-tile.mdx) is a Value with a label, a time and a surface of its own.
* [RangeBar](./range-bar.mdx) is a Value with its reference range drawn around it.
* [Callout](./callout.mdx) owns the framing a bare risk proportion needs before it can be shown.
