NumberField
A labeled numeric input with stepper buttons, formatting, and validation.
import { NumberField } from '@ontoui/react';
<NumberField.Root defaultValue={1}> <NumberField.Label>Quantity</NumberField.Label> <NumberField.Group> <NumberField.Decrement /> <NumberField.Input /> <NumberField.Increment /> </NumberField.Group></NumberField.Root>;The input holds a number, not a string: value, defaultValue, and onValueChange all deal in
number | null, where null means the input is empty. Arrow keys step the value, Shift steps by
largeStep, and the meta key steps by smallStep.
Anatomy
| Part | Description |
|---|---|
NumberField.Root | Container that groups all parts and holds the value |
NumberField.Label | Accessible label associated with the input |
NumberField.Group | Bordered row that wraps the input and its stepper buttons |
NumberField.Decrement | Button that lowers the value by one step |
NumberField.Input | The underlying <input> element |
NumberField.Increment | Button that raises the value by one step |
NumberField.ScrubArea | Optional region that changes the value when dragged |
NumberField.Description | Optional helper text displayed below the group |
NumberField.Error | Error message shown when the field is invalid |
NumberField.Decrement and NumberField.Increment are labelled “Decrease” and “Increase” and are
kept out of the tab order — they duplicate what the arrow keys already do. Pass aria-label to
translate them, and children to replace the default icons.
Examples
Range and step
min and max clamp the value; the stepper buttons disable themselves at each bound. step sets
how much a single press or arrow key moves the value.
0–100, in steps of 5.
<NumberField.Root defaultValue={10} min={0} max={100} step={5}> <NumberField.Label>Discount</NumberField.Label> <NumberField.Group> <NumberField.Decrement /> <NumberField.Input /> <NumberField.Increment /> </NumberField.Group> <NumberField.Description>0–100, in steps of 5.</NumberField.Description></NumberField.Root>Formatting
format takes Intl.NumberFormat
options, so the input can display a currency or a percentage while onValueChange still reports a
plain number. Formatting follows the user’s runtime locale unless you set locale.
<NumberField.Root defaultValue={1200} step={100} format={{ style: 'currency', currency: 'USD' }}> <NumberField.Label>Budget</NumberField.Label> <NumberField.Group> <NumberField.Decrement /> <NumberField.Input /> <NumberField.Increment /> </NumberField.Group></NumberField.Root>A percent-formatted field stores the fraction: 0.15 displays as 15%.
Scrub area
Wrap the label in NumberField.ScrubArea to let the user drag it sideways to change the value —
useful for design-tool controls where fine adjustment matters more than typing.
Drag the label sideways to change the value.
<NumberField.Root defaultValue={24}> <NumberField.ScrubArea> <NumberField.Label>Font size</NumberField.Label> </NumberField.ScrubArea> <NumberField.Group> <NumberField.Decrement /> <NumberField.Input /> <NumberField.Increment /> </NumberField.Group></NumberField.Root>Set direction="vertical" to scrub up and down instead. Scrubbing takes a pointer lock and hides
the real cursor, so the component draws a replacement one; Safari keeps its native cursor because
the pointer-lock notification shifts the layout there.
Scrubbing is a pointer-only affordance. The arrow keys and the stepper buttons remain the accessible path to the same values, so never make a scrub area the only way to reach one.
Invalid
Set invalid on NumberField.Root to mark the field as invalid and reveal NumberField.Error.
<NumberField.Root invalid defaultValue={0} min={1}> <NumberField.Label>Seats</NumberField.Label> <NumberField.Group> <NumberField.Decrement /> <NumberField.Input /> <NumberField.Increment /> </NumberField.Group> <NumberField.Error>Book at least one seat.</NumberField.Error></NumberField.Root>To react to native validation instead of driving it yourself, drop invalid and give each error a
match:
<NumberField.Error match="rangeUnderflow">Book at least one seat.</NumberField.Error><NumberField.Error match="valueMissing">Enter a number of seats.</NumberField.Error>Disabled
Set disabled on NumberField.Root to disable the input and both stepper buttons.
<NumberField.Root disabled defaultValue={5}> <NumberField.Label>Licences</NumberField.Label> <NumberField.Group> <NumberField.Decrement /> <NumberField.Input /> <NumberField.Increment /> </NumberField.Group></NumberField.Root>API Reference
NumberField.Root
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
style? | CSSProperties | — |
id? | string | — |
name? | string | — |
value? | number | null | — |
defaultValue? | number | — |
min? | number | — |
max? | number | — |
step? | number | "any" | — |
largeStep? | number | — |
smallStep? | number | — |
format? | NumberFormatOptions | — |
locale? | LocalesArgument | — |
allowWheelScrub? | boolean | — |
snapOnStep? | boolean | — |
disabled? | boolean | — |
readOnly? | boolean | — |
required? | boolean | — |
invalid? | boolean | — |
onValueChange? | (value: number | null, eventDetails: NumberFieldRootChangeEventDetails) => void | — |
onValueCommitted? | (value: number | null, eventDetails: NumberFieldRootCommitEventDetails) => void | — |
ref? | Ref<HTMLDivElement> | — |
NumberField.Label
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
ref? | Ref<HTMLDivElement> | — |
NumberField.Group
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
ref? | Ref<HTMLDivElement> | — |
NumberField.Decrement
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
aria-label? | string | — |
ref? | Ref<HTMLDivElement> | — |
NumberField.Input
| Prop | Type | Default |
|---|---|---|
className? | string | — |
placeholder? | string | — |
aria-label? | string | — |
ref? | Ref<HTMLDivElement> | — |
NumberField.Increment
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
aria-label? | string | — |
ref? | Ref<HTMLDivElement> | — |
NumberField.ScrubArea
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
direction? | "horizontal" | "vertical" | horizontal |
pixelSensitivity? | number | — |
teleportDistance? | number | — |
ref? | Ref<HTMLDivElement> | — |
NumberField.Description
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
ref? | Ref<HTMLDivElement> | — |
NumberField.Error
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
match? | boolean | keyof ValidityState | — |
ref? | Ref<HTMLDivElement> | — |