Components · Forms

Numeric input

Spin box and slider. One is for entering a number the clinician knows; the other is for exploring a range where the exact value does not matter — and confusing them puts an approximation into a clinical record.

Stable · v1.0 shadcn/ui WCAG 2.2 AA

Overview

A slider is a fast, imprecise, satisfying control. Those first two properties are in tension in clinical software, and precision wins: a value a clinician knows is typed, not dragged towards.

ControlUse whenNever for
Spin box A specific value is being entered or adjusted — a measurement, a threshold, a count Ranges so large that stepping is impractical
Slider A view parameter where feedback is continuous and the exact number does not matter — gain, sweep speed, zoom, playback position Any value that will be recorded
Sliders never enter clinical data

A slider cannot be operated precisely with gloves, on a moving cart, or with a tremor, and it has no state in which it is "unanswered" — it always shows a value, so a form submitted untouched records whatever it happened to be pointing at. Any number that will be stored, trended or acted on is entered as a spin box or text field. Sliders are for view parameters only, where the effect is immediately visible and nothing is written down.

Typed first

Every numeric control accepts a typed value. Steppers assist; they never replace the keyboard.

Never silently changed

No scroll wheel, no drag-past-the-limit, no rounding that happens without saying so.

Unit-bound

The unit is part of the control, visible at all times — the same rule as Key–value pair.

Anatomy

Spin box · slider
mm
0.5–5.0 mm · steps of 0.1 · administrative setting
Display gain
5 mm/mV10 mm/mV20 mm/mV

A view parameter. Nothing is recorded, and the current value is stated on the trace — see ECG review.

Rules

Do
ng/L

Typed, unit-bound, precise. The steppers are a convenience, not the route.

Don't
Peak troponin
03000 ng/L

A recorded clinical value on a slider. Untouched it still reads something, and nobody can hit 1 240 gloved.

What the field must let you type

A dose is entered one character at a time, and the characters on the way to a number are not numbers. 1. is what exists between the 1 and the 5 of 1.5. A control that treats every keystroke as a finished value will parse that as 1, redraw the field as 1, and the decimal point is gone before the 5 arrives.

The rule

What is on screen is what was typed, until the clinician leaves the field. The committed value and the characters in the box are two different things, and a control that conflates them edits a dose while it is being written.

This component did conflate them. It rendered the committed number, so entering 1.5 produced 15 — a tenfold dosing error manufactured by the control, in the file whose own first comment calls it “how a number that changes what a device does to a body gets entered”. It survived because every test set the value in one operation. Nobody had typed into it.

Do

1. → 1.5

The point stands while the number is being written.

The field holds what was typed. The number is read from it, not written back over it.

Don't

1. → 1 → 15

Ten times the intended dose, entered correctly.

Nothing was mistyped and nothing warned. The control parsed an unfinished number and redrew the field from it.

Naked decimals and trailing zeros

Writing for patients has long carried the rule that .5 is a tenfold error waiting to happen and that 5.0 read quickly is fifty. It had never reached the control where doses are actually typed.

The rule

A decimal always carries its leading zero and never carries a trailing one. 0.5, not .5. 5, not 5.0. The point is the smallest mark on the screen and the one whose loss costs the most.

What the component enforces, and what it only asks for

This is the strictest control in the package and the split is unusually favourable: nearly everything about how a number is entered is held here. What is not held is what the number means.

RuleHeld by How
What is on screen is what was typedThe component The draft is held apart from the committed value, so an unfinished number is not parsed and rewritten — which is what turned 1.5 into 15.
Asserted: NumericInput: typing a decimal produces the decimal
Keystrokes are never silently droppedThe component Text the field cannot read stays on screen and is named as unreadable, including a comma decimal separator.
Asserted: NumericInput: a word is reported, not swallowed
Exponent and hex notation cannot be enteredThe component Parsed by hand rather than with Number(), which accepts 1e5, 0x10 and Infinity.
Asserted: parseDecimal: refuses what Number() would have accepted
Nothing is rewritten when the field is leftThe component No clamping on blur. An out-of-envelope entry stays as typed with the refusal beside it.
Asserted: NumericInput: a refused entry stays on screen as typed
A decimal carries its leading zero and no trailing oneThe component Normalised on blur only, so the point is left alone while it is being written.
Asserted: NumericInput: a naked decimal gains its zero, a trailing zero loses it
A unit is always shownThe component unit is required. A dose without one is not expressible.
Asserted: NumericInput: the unit is always rendered
A step acts on the committed value and never moves it the wrong wayThe component The steppers step from what is committed, never from an entry that was refused or unreadable. They used to step from whatever was on screen: with 40 committed and 250 typed into a 1–99 setting, Decrease committed 99. A step that clamping would send the opposite way to the press does nothing. Driven in a browser.
Asserted: NumericInput: decrease never raises the committed value
Only a setting is clampedThe component For an observation the envelope is the usual range, not a limit, and stepping a recorded 250 no longer pulls it to 100.
Asserted: NumericInput: stepping an observation does not pull it into the usual range
A decimal step lands on the decimalThe component Rounded to the precision of the step and the value, so 0.2 plus 0.1 is 0.3 and not 0.30000000000000004 in a dose field.
Asserted: NumericInput: a decimal step lands on the decimal, not a float artefact
The unit is part of the field’s nameThe component Read from the accessibility tree: the dose field is named “Dose (mg)”. It was named “Dose”, with the unit in a box beside it that nothing referenced.
Asserted: AX tree: a numeric field's unit is in its accessible name
min and max are the right numbersYou The component enforces the envelope you give it. Whether that envelope is the device's limit or this patient's usual range is kind, and whether the numbers are correct is yours.
A confirmed out-of-range observation is acted onYou The component records it and says so. What happens next — a second check, a note, a flag — is your workflow.
The step matches a clinically meaningful incrementYou step is a number you pass. A stepper moving in 1 mg on a drug titrated in 0.5 is a control that cannot express half the doses.
Why this is written down

A rule stated on a component page reads as a rule the component keeps. 6 of these 9 are; 3 are obligations this page was quietly handing to the reader. Six of nine are held, which is the highest proportion on any page here. The three that are not are all about clinical meaning, and no type can check that.

Do's and don'ts

Do
ng/L

Typed, unit-bound, precise. Steppers state their increment in the accessible name.

Don't
Peak troponin
03000 ng/L

A recorded clinical value on a slider. Untouched it still reads something, and nobody hits 1 240 gloved.

Accessibility

Outcomes of use

What this contributes to, in the terms of Usability & context of use. These are attributes believed to contribute to an outcome; the outcome itself is settled by observing real use in a specified context, not by this page.

Clinical safety notes

Risk controls this page specifies

Trace these in your risk file (ISO 14971) and usability engineering file (IEC 62366-1). Which of these the component keeps and which are yours is stated in the guarantees table; a control the component does not keep is one your risk file must show you keep.

Implementation

$ npx shadcn@latest add https://md.notjustany.tech/r/numeric-input.json
<NumericInput
  label="Alarm threshold — ST elevation"
  unit="mm"                 // required: a dose without a unit is not a dose
  kind="setting"            // required: decides what min and max mean
  min={0.5} max={5.0} step={0.1}
  value={value} onChange={setValue}
/>

// There is no slider in this package. A recorded clinical value entered by
// dragging cannot be reproduced, so the control does not exist to be misused.
NotJustAnyMed.Tech Design System · Numeric input · v1.0 · draft for review
Covers Spin box and Slider. Reference applications named in this system are fictional; all values shown are fabricated.