ReadingInput
A field for typing in a measurement, with the unit shown and switchable beside the number, and room for an advisory the product wrote.
Preview
reading-input · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/reading-input
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 { ReadingInput } from "@/components/ui/reading-input"<ReadingInput
label="Example measurement"
unit={unit}
units={["kg", "lb", "st"]}
precision={1}
value={reading}
onChange={(next) => {
setReading(next.value)
setUnit(next.unit)
}}
/>When to use it
Use it when
- Anywhere a person types a measurement of themselves: weight, blood pressure, glucose, temperature.
- Entry where the unit is part of the value.
- Entry where the product flags an unlikely number without stopping anybody.
Do not use it when
- A quantity, a count or a code, not a measurement of a person.Use
number-fieldinstead. - The value is best chosen from a small set rather than typed.Use
segmented-controlinstead. - The reader estimates the value, such as a pain score, not measures it.Use
scale-inputinstead. - You are displaying a reading, not collecting one.Use
valueinstead. - Several related measurements captured in one interaction.Use
log-sheetinstead.
Clinical meaning
Asserts. That the reader typed this number, in this unit, as a measurement of themselves. It is always self-reported.
Never read as. A validated measurement, a device reading or an assessed value.
Colour axis. Neither; the advisory is ordinary foreground.
Thresholds. None. The component holds no bound. The product
supplies the finished warning, which never sets aria-invalid, moves focus or blocks
submission. Staleness belongs to
Uncertainty, staleness and missing data.
Unit switches. Only definitional conversions from tokens/units.json: kg, lb, st,
°C and °F. mmol/L to mg/dL is refused, because the factor is the substance's molar mass,
so that switch clears the entry and says so. See
Unit systems and
Numbers, units and precision.
Anatomy
ReadingInputA div for a single reading, a fieldset for a compound one.FieldEmbedded whole. Owns the label, descriptions, invalid state and 44px floor.Controlled bylabelReadingInput.LegendCompound only: the measurement's name, as a legend.Controlled bylabelReadingInput.HintCompound only: the group's guidance, in every box's description.Controlled byhintReadingInput.ControlThe row of boxes and the unit. It wraps at 200% text.ReadingInput.SegmentOne labelled box carrying data-opsinjs-value, one per part.Controlled bysegmentsReadingInput.UnitA native select at the 44px floor, or plain text for one unit.Controlled byunitsReadingInput.WarningThe product's advisory, as a description rather than an error.Controlled bywarningReadingInput.EffectWhat a switch did: converted, cleared or restored. Gone once a box is touched.
Examples
Two numbers that are one measurement
segments turns the field into a <fieldset> with a <legend>, one box per part.
reading-input-a-compound-reading · base base · style base-lyraOpen under the product themeSwitching the unit, and the sentence that follows it
precision sets the decimal places a conversion rounds to.
reading-input-switching-the-unit · base base · style base-lyraOpen under the product themeA sentence the product wrote
warning shows the product's sentence without comparing anything.
reading-input-a-warning-the-product-owns · base base · style base-lyraOpen under the product themeContent guidelines
Label the measurement in the reader's words and put the unit beside the field, not in the label. Hints show the shape of an answer, never a sample reading and never a bound. Advisories ask a question and offer the likely fix.
Do
"That is a long way above most readings. Did you mean to type a decimal point?"
Don’t
"Invalid value. That reading is outside the accepted range." blames the reader.
Accessibility
Audited against WCAG 2.2 AA in a source pass and a rendered pass, and
pnpm run check:a11y runs on every commit. The author ran the audit, so it is not
independent, and clinical review is pending.
Fixed in this pass. autoComplete is now a prop reaching Field.Control, defaulting
to "off", because a self-reported reading has no WCAG autofill token and the component
invents none (1.3.5).
From the source. Field renders a real <label for>, and a compound reading is a
<fieldset> of one Field per part. The unit's spoken form joins the box's description,
each option carries its plural on aria-label, and the box is type="text" with
inputMode defaulting to decimal. The advisory and the effect line are descriptions:
no aria-invalid and no focus move.
Left to the product. The effect line reporting a switch sits in no live region, so a
product must wire effectText to a polite announcer. Without that, a non-sighted reader
hears the new unit but not that the number changed.
Not gated. No hit area is measured, and the boundary inherits --border without a
gated pair.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves to the box, then the unit switch | Three stops on a compound reading: two boxes and the switch. |
| Shift+Tab | Moves backwards through the same stops | Reverse order matches the visual order. |
| Any digit, . or , | Types into the focused box | Nothing is rejected, clamped or reformatted while typing. |
| Up / Down | Changes the unit when the switch has focus | Native select. The entry converts or clears, and the line says which. |
| Enter | Whatever the surrounding form does | Nothing is swallowed. Set enterKeyHint to name the return key's action. |
| Escape | Nothing here | No overlay to dismiss, no entry to revert. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| 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 |
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 the root, the control row, every segment and the unit, always. | reading-input, reading-input-control, reading-input-segment, reading-input-unit |
| data-slot | Legend and hint on a compound reading, warning when supplied, effect after a switch. | reading-input-legend, reading-input-hint, reading-input-warning, reading-input-effect |
| data-opsinjs-value | On every ReadingInput.Segment, always | The number in that box, read off the box not the prop |
| data-status, data-category | Never, at any value of any prop | Never set |
API reference
Prop
Type
Generated from ReadingInputProps in registry/bases/base/reading-input.tsx.
unit is required and value is in it. Without precision a conversion is not rounded.
There is no error prop: an error belongs to Field.
Related
- Field is what this is built on, without a unit or an advisory.
- NumberField is a plain numeric input with no clinical semantics.
- Value is the display counterpart, and both must agree on precision and units.
- ResultCard is where a compound reading goes, in the same
segmentsshape. - ScaleInput is for estimated scores such as pain, which are not measurements.
LogSheet
A small sheet that slides up so you can record something in a few seconds without leaving the screen you were on. One reading, one dose, one note.
BodyMap
A neutral, non-realistic body schematic for pointing at where something is. It records region keys and interprets nothing, so the product owns every meaning.