NumberInput
A field for an exact number with step buttons and arrow-key stepping, formatted for the locale — the APG spinbutton on top of Input.
Basic usage
Install the package:
pnpm add @design-schema/react @design-schema/tokens Import the stylesheet once for the app, then the component:
import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { NumberInput } from '@design-schema/react'; Examples
import { NumberInput } from '@design-schema/react';
<NumberInput label="Quantity" name="quantity" />Props
22 rows| Prop | Type | Default | Description |
|---|---|---|---|
| label required | string | — | Visible label. Accessibility: label/for on the input; accessibilityLabel on native. |
| name required | string | — | Field name for the Form. No platform's Form (web/rn FormContext, Lit DsFormField/ds-form) has a number type, so the value registers as its plain decimal string — String(value), "." decimal, no grouping, currency symbol, percent sign or affixes ("1234.5", "25" for 25%) — and an empty or disabled field registers nothing (undefined on web/rn, null currentValue on Lit). Consumers parse it back with Number(). `form.valueType: number` names what the field means, not how it is registered: the decimal string is the wire form on every platform, and a `valueAsNumber` accessor (Lit) or the event payload carries the number. |
| value | number | — | Controlled numeric value, typed `number | null | undefined`: `null` is a controlled empty field, `undefined` means uncontrolled (defaultValue applies). While the input is focused it shows the raw typed text, so typing is never swallowed; the controlled value takes over the display on blur/Enter (re-formatted), on every step, and whenever the prop changes to a number different from the parsed typed text. A controlled change to `null` while the user is typing keeps the typed text until blur/Enter. Typed text wins over the value whenever it exists, which after a commit is only the non-numeric case; every numeric commit hands the display back to the formatted value. Inside a Form, validation reads the prop value, so a step or Enter is validated against the new number only once the owner re-renders. |
| defaultValue | number | — | Initial value. |
| min | number | — | Lower bound; values are clamped on blur and the decrement button disables at it. |
| max | number | — | Upper bound. |
| step | number | 1 | Increment for the buttons and arrow keys. When `precision` is omitted, values round to the number of decimals in `step` (step 0.25 → 2 places); values never snap to multiples of `step`, so a typed 12 with step 5 stays 12. |
| precision | number | — | Decimal places to keep and display. Defaults to the decimals in `step`. Under `format: percent` it measures the displayed side (the number the user sees and types, not the divided-by-100 one), so it is the formatter's minimum and maximum fraction digits: step 5 shows "10%", step 0.5 shows "10.5%". |
| format | decimal | currency | percent | unit | decimal | Locale formatting of the displayed value via Intl.NumberFormat: thousands separators, currency symbol (`currency` prop), percent, or a unit (`unit` prop). The underlying value is always a plain number. `unit` with no `unit` prop at all formats as plain decimal with no suffix (only an unrecognised unit string falls back to `trailingText`). |
| currency | string | — | ISO 4217 code for `format: currency` (e.g. USD). |
| unit | string | — | Intl unit identifier for `format: unit` (e.g. kilogram, hour). A string Intl does not know is shown as `trailingText` (the `suffix` part) when `trailingText` is not given, with plain decimal formatting. |
| leadingText | string | — | Static text before the value inside the field ("$"), when `format` cannot express it. (Not `prefix`: that name is a native Element member.) |
| trailingText | string | — | Static text after the value inside the field ("kg", "%"). Also the literal shown when `unit` is not a valid Intl unit. Ignored under `format: percent`, which draws its own sign, the way `leadingText` is ignored under `currency`: a field never shows two. |
| hideSteppers | boolean | false | Hide the increment/decrement buttons. Arrow keys work regardless. |
| placeholder | string | — | Example value shown while empty. |
| description | string | — | Helper text. |
| required | boolean | false | Must have a value to submit. |
| hideLabel | boolean | false | Visually hide the label (it remains the accessible name). Only for a field whose context already names it: a DataGrid cell editor, a Search. On React Native there is no label element to hide (and no `<label for>`), so the label part simply is not rendered and `copy.requiredIndicator` survives only inside the accessibilityLabel. |
| size | sm | md | md | sm for fields inside grid cells and toolbars: minimum target height, tighter padding, small type. |
| disabled | boolean | false | Not editable, not submitted, still readable. On web and Lit the input stays focusable as `readonly` with aria-disabled (not natively disabled); on React Native it is `editable={false}` with accessibilityState.disabled, as Input. |
| invalid | boolean | false | Marks the field invalid. |
| error | string | — | Error message; implies invalid. |
Events
1 row| Event | Type | Description |
|---|---|---|
| onChange | onChange / change / onChangeText | Fired when the numeric value changes (on each valid keystroke, step, and on blur after clamping/rounding), with the number or undefined. |
Accessibility
Decisions this component leaves to you
Supply the label. It is associated with the control for you, but the words are yours — a placeholder is not a label.
Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.
Supply the error text when the value is invalid. The component links and announces it; it cannot write it.
Required props that carry accessibility meaning: label. Each is described in the props table above.
Implicit ARIA role: spinbutton.
Anatomy
What generation guarantees
Label association — The label is programmatically associated with the control, not merely placed beside it.
Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.
Error identification — An invalid value is identified in text and linked to the control — never signalled by colour alone.
Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.
Arrow navigation — Arrow keys move between items; Home and End jump to the ends.
Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.
Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.
Target size — Interactive targets are at least 44×44 CSS pixels — the touch-first size, not the minimum.
This component does not animate, so it has no reduced-motion behaviour to describe.