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.
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.
| Control | Use when | Never 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 |
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
A view parameter. Nothing is recorded, and the current value is stated on the trace — see ECG review.
Rules
- Never
type="number". Its scroll-wheel behaviour changes values silently and its spinners are too small to hit. Usetype="text"withinputmode="decimal"— the same rule as Text field. - Stepper buttons meet the touch floor and state their step in the accessible name: "Increase by 0.1 mm", not "Increase".
- What out-of-range means depends on what the number does. A
kindofsettingcommands a device, so the envelope is a limit and a value outside it is not committed. Akindofobservationrecords what was found, so the envelope is the usual range and a value outside it is recorded with a note — the patient may genuinely have a troponin of 25 000, per Text field. The prop is required because the same two numbers meant opposite things depending on which the caller had in mind, and this page stated one policy while the component implemented the other. - Rounding is stated. If entry is rounded to the step, say so on blur rather than silently rewriting what was typed.
- Sliders show their current value as text, always, and never rely on knob position alone.
- Sliders snap to documented steps — gain moves between real settings, not continuously to a value no clinician can reproduce.
- Non-default view parameters are flagged wherever the output is read, so a colleague knows they are not looking at standard calibration.
Typed, unit-bound, precise. The steppers are a convenience, not the route.
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.
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.
- Keystrokes are never dropped, and never silently ignored. Text the field cannot read stays on screen and is named as unreadable. The previous version discarded anything that failed to parse without a sound, which in a comma-decimal locale meant 1,5 was simply refused character by character.
- A comma is a decimal separator. Most of the locales this system claims to support write it that way, and a control that only accepts a point is not refusing a mistake — it is refusing a convention.
Number()is not a parser for this. It accepts 1e5, 0x10, Infinity and a leading plus. Every one of those was reachable in a dose field, and none is a dose anybody typed on purpose.- Nothing is rewritten on blur. An entry outside the envelope stays exactly as typed with a refusal beside it. This control used to clamp silently — 250 became 99 with nothing said — while the rule below required rounding to be stated, and the component's own message said the value could not be set.
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.
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.
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.
- Normalisation happens when the field is left, not while it is being typed. Inserting a zero in front of a point the clinician is still writing moves the caret and rewrites the entry — the defect above, wearing a safety rule as a disguise.
- This is a convention with published guidance behind it, widely adopted in medication-safety practice. Convention rather than Standard: it is a writing rule this system adopts, not a conformance requirement it can cite a clause for.
- It applies wherever the number is displayed, not only where it is entered — a dose echoed into a confirmation, a label or a printout carries the same form.
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.
| Rule | Held by | How |
|---|---|---|
| What is on screen is what was typed | The 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 dropped | The 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 entered | The 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 left | The 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 one | The 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 shown | The 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 way | The 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 clamped | The 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 decimal | The 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 name | The 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 numbers | You | 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 on | You | 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 increment | You | 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. |
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
Typed, unit-bound, precise. Steppers state their increment in the accessible name.
A recorded clinical value on a slider. Untouched it still reads something, and nobody hits 1 240 gloved.
Accessibility
- The shipped field is a text input with two step buttons, not a spinbutton.
Each button is named with its direction, its step and its unit — “Increase Dose
by 0.5 mg” — and the field is described by its range, then by its refusal. It
does not take
role="spinbutton", because that role promises arrow-key stepping, and an arrow pressed while the caret is in a dose field changes the dose without the reader looking at it — the scroll-wheel hazard, one key over. This bullet used to describe a spinbutton the component did not ship. - Slider uses
role="slider"witharia-valuetextcarrying the unit — "10 millimetres per millivolt", not "10". - Both are keyboard-complete. No numeric control depends on dragging.
- Slider knob meets the physical touch floor, which usually means a longer track rather than a bigger knob — see Scaling.
- Never trap the scroll wheel. Scrolling over a numeric control scrolls the page.
- Value announced on change, not on every intermediate step while dragging.
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.
- Effectiveness — the number recorded is the number meant, to the precision meant, in the units meant.
- Efficiency — typing a known value beats stepping to it; the steppers exist for adjustment, not entry.
- Satisfaction — confidence that nothing changed while scrolling past. That doubt, once earned, makes a clinician re-check every numeric field on the screen.
Clinical safety notes
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.
- Sliders never carry recorded clinical values. (the component) Mitigates: an approximate or untouched value entering the record.
- No
type="number", no scroll-wheel capture. (the component) Mitigates: a value silently changed by a scroll gesture. - Unit bound to the control. (the component) Mitigates: order-of-magnitude error from an assumed unit.
- Out of range confirms rather than rejects. (the component) Mitigates: a true extreme value being un-enterable.
- Rounding disclosed. (the component) Mitigates: a stored value differing from the one typed without the clinician knowing.
- Steppers state their step size. (the component) Mitigates: adjusting by an unexpected increment.
- Non-default view parameters flagged at the point of reading. (you) Mitigates: misinterpreting amplitude because gain was changed.
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.
Related
- Text field — the input rules underneath.
- Key–value pair — how the result is displayed.
- ECG review — gain as a view parameter.
- Scaling & displays — target sizes.