Skip to content

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

PartDescription
NumberField.RootContainer that groups all parts and holds the value
NumberField.LabelAccessible label associated with the input
NumberField.GroupBordered row that wraps the input and its stepper buttons
NumberField.DecrementButton that lowers the value by one step
NumberField.InputThe underlying <input> element
NumberField.IncrementButton that raises the value by one step
NumberField.ScrubAreaOptional region that changes the value when dragged
NumberField.DescriptionOptional helper text displayed below the group
NumberField.ErrorError 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.

Book at least one seat.
<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>