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
PropTypeDefaultDescriptionPlatforms
label requiredstring—

Visible label.

Accessibility: label/for on the input; accessibilityLabel on native.

web, lit, rn, swiftui
name requiredstring—

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.

web, lit, rn, swiftui
valuenumber—

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.

web, lit, rn, swiftui
defaultValuenumber—

Initial value.

web, lit, rn, swiftui
minnumber—

Lower bound; values are clamped on blur and the decrement button disables at it.

web, lit, rn, swiftui
maxnumber—

Upper bound.

web, lit, rn, swiftui
stepnumber1

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.

web, lit, rn, swiftui
precisionnumber—

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%".

web, lit, rn, swiftui
formatdecimal | currency | percent | unitdecimal

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

web, lit, rn, swiftui
currencystring—

ISO 4217 code for `format: currency` (e.g. USD).

web, lit, rn, swiftui
unitstring—

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.

web, lit, rn, swiftui
leadingTextstring—

Static text before the value inside the field ("$"), when `format` cannot express it. (Not `prefix`: that name is a native Element member.)

web, lit, rn, swiftui
trailingTextstring—

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.

web, lit, rn, swiftui
hideSteppersbooleanfalse

Hide the increment/decrement buttons. Arrow keys work regardless.

web, lit, rn, swiftui
placeholderstring—

Example value shown while empty.

web, lit, rn, swiftui
descriptionstring—

Helper text.

web, lit, rn, swiftui
requiredbooleanfalse

Must have a value to submit.

web, lit, rn, swiftui
hideLabelbooleanfalse

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.

web, lit, rn, swiftui
sizesm | mdmd

sm for fields inside grid cells and toolbars: minimum target height, tighter padding, small type.

web, lit, rn, swiftui
disabledbooleanfalse

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.

web, lit, rn, swiftui
invalidbooleanfalse

Marks the field invalid.

web, lit, rn, swiftui
errorstring—

Error message; implies invalid.

web, lit, rn, swiftui

Events

1 row
EventTypeDescriptionPlatforms
onChangeonChange / change / onChangeText

Fired when the numeric value changes (on each valid keystroke, step, and on blur after clamping/rounding), with the number or undefined.

web, lit, rn, swiftui

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

labeldescriptionfieldinputdecrementButtonincrementButtonprefixsuffixerrorMessage

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.

NumberInput keyboard support

5 rows
KeyAction
ArrowUpIncreases by `step` (clamped to max).
ArrowDownDecreases by `step` (clamped to min).
PageUp or PageDownChanges by ten steps (10 × `step`, clamped); the 10 is a count, not a style value.
Home or EndSets min / max when they are defined (component code, not native: a text input cannot do this); otherwise the key is left to the input's native caret movement. A jump is a step: it clears a clamp message and fires onChange only when the value changes.
EnterCommits (rounds and clamps) the typed value; inside a Form, submits (on React Native too: returnKeyType done, no next-field chain, submitBehavior blurAndSubmit). Outside a Form it commits and nothing else — focus and the native keyboard stay (React Native submitBehavior submit).

Contrast pairs

4 rows
ForegroundBackgroundLevel
color.foregroundcolor.backgroundAA
color.foreground.mutedcolor.backgroundAA
color.foreground.dangercolor.backgroundAA
color.border.strongcolor.backgroundAA (non-text)

This component does not animate, so it has no reduced-motion behaviour to describe.