Number Input
A text field for a number, with keyboard stepping, optional increment and decrement buttons, and locale-aware formatting.
<NumberInput
defaultValue={5}
min={1}
max={50}
ariaLabel="Failed logins before lockout"
/>Usage
import { NumberInput } from "@zenoxai/ui/components/number-input"
<NumberInput defaultValue={5} min={1} max={50} ariaLabel="Failed logins before lockout" />
NumberInput is a single boxed input on Base UI’s Number Field. It accepts the Number Field
root props: value, defaultValue, onValueChange, min, max, step, format, name,
required, disabled, and size. Three props of its own reach the inner <input>:
ariaLabel, inputClassName and inputRef. className and any other attribute go to the
wrapper. The field has the height, padding, fill, focus ring and disabled look of an
Input.
For stepper buttons, compose the parts instead: NumberInputRoot, NumberInputGroup,
NumberInputDecrement, NumberInputField and NumberInputIncrement.
Use it for counts, thresholds, ports and durations: values the user adjusts by small amounts or types exactly. For a value picked roughly from a range, use a Slider. For numbers that are identifiers (a ZIP code, an account number), use an Input: they are not quantities and must not be stepped or formatted.
Examples
With a label
id on NumberInput goes to the <input>, so a FieldLabel from
Field can point at it. The input is 80px wide; widen it with
inputClassName when the formatted value is longer.
Events older than this are deleted.
<Field className="w-fit">
<FieldLabel htmlFor="retention-days">Retention</FieldLabel>
<NumberInput
id="retention-days"
defaultValue={90}
min={7}
max={365}
format={{ style: "unit", unit: "day", unitDisplay: "long" }}
inputClassName="w-28"
/>
<FieldDescription>Events older than this are deleted.</FieldDescription>
</Field>With buttons
NumberInputGroup draws one border around the buttons and the field. The buttons are named
“Decrease” and “Increase”; pass aria-label to translate them. Holding a button repeats the
step.
<NumberInputRoot defaultValue={3} min={1} max={10}>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField aria-label="Concurrent scans" className="w-12" />
<NumberInputIncrement />
</NumberInputGroup>
</NumberInputRoot>Sizes
size on NumberInput or NumberInputRoot sets the height of the field and its buttons: sm
(28px), default (32px) and lg (36px), the heights of Input and Button. The group is as tall
as an Input of the same size, border included, and the stepper icons follow the size (14px at
sm, 16px otherwise).
<div className="flex flex-wrap items-center gap-3">
{(["sm", "default", "lg"] as const).map((size) => (
<NumberInputRoot key={size} size={size} defaultValue={3} min={1}>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField
aria-label={`Concurrent scans, ${size}`}
className="w-12"
/>
<NumberInputIncrement />
</NumberInputGroup>
</NumberInputRoot>
))}
</div>Formatted
format takes Intl.NumberFormat options: percent, currency, units, fraction digits. The
value stays a plain number (0.8 for 80%); only the text in the field is formatted, in the
user’s locale unless you pass locale.
<>
<NumberInput
defaultValue={0.8}
min={0}
max={1}
step={0.05}
format={{ style: "percent" }}
ariaLabel="Detection confidence threshold"
/>
<NumberInput
defaultValue={1200}
min={0}
step={100}
format={{
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}}
ariaLabel="Monthly scan budget"
inputClassName="w-24"
/>
</>Controlled
onValueChange receives a number, or null when the field is empty. Type the state as
number | null and handle the empty case.
Assets are scanned every 6 hours.
function NumberInputControlled() {
const [hours, setHours] = React.useState<number | null>(6)
return (
<div className="grid justify-items-center gap-3">
<NumberInputRoot value={hours} onValueChange={setHours} min={1} max={24}>
<NumberInputGroup>
<NumberInputDecrement />
<NumberInputField
aria-label="Scan interval in hours"
className="w-12"
/>
<NumberInputIncrement />
</NumberInputGroup>
</NumberInputRoot>
<p className="text-sm text-text-secondary">
{hours === null
? "Enter an interval."
: `Assets are scanned every ${hours} ${hours === 1 ? "hour" : "hours"}.`}
</p>
</div>
)
}Accessibility
- The control is a text input with
aria-roledescription="Number field", so screen readers read the formatted text (“80%”). Name it withariaLabel, or withidand aFieldLabel. A plainaria-labelonNumberInputlands on the wrapper, not on the input. - ArrowUp and ArrowDown change the value by
step, with Shift bylargeStep(10 by default) and with Alt bysmallStep(0.1 by default). Home and End jump tominandmax. - The value is clamped to
minandmaxwhen the field loses focus.
API reference
Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.
NumberInput
NumberInput is a field for a number, with arrow-key stepping and clamping to `min` and `max`, built on Base UI NumberField. On its own it renders one boxed input; for stepper buttons, compose the parts instead:
| Prop | Type | Default |
|---|---|---|
allowOutOfRange | booleanWhen true, direct text entry may be outside the `min`/`max` range without clamping, so native range underflow/overflow validation can occur. Step-based interactions (keyboard arrows, buttons, wheel, scrub) still clamp. | false |
allowWheelScrub | booleanWhether to allow the user to scrub the input value with the mouse wheel while focused and hovering over the input. | false |
ariaLabel | stringAccessible name of the input, when no visible label points at it. | - |
defaultValue | numberThe uncontrolled value of the field when it's initially rendered. | - |
disabled | booleanWhether the component should ignore user interaction. | false |
form | stringIdentifies the form that owns the hidden input. Useful when the number field is rendered outside the form. | - |
format | NumberFormatOptionsOptions to format the input value. | - |
id | stringThe id of the input element. | - |
inputClassName | stringClasses for the input; `className` styles the root. | - |
inputRef | Ref<HTMLInputElement>A ref to access the hidden input element. Ref to the input element, to focus it from outside. | - |
largeStep | numberThe large step value of the input element when incrementing while the shift key is held. Snaps to multiples of this value when `snapOnStep` is enabled. | 10 |
locale | LocalesArgumentThe locale of the input element. Defaults to the user's runtime locale. | - |
max | numberThe maximum value of the input element. | - |
min | numberThe minimum value of the input element. | - |
name | stringIdentifies the field when a form is submitted. | - |
onValueChange | ((value: number | null, eventDetails: NumberFieldRootChangeEventDetails) => void)Callback fired when the number value changes. | - |
onValueCommitted | ((value: number | null, eventDetails: NumberFieldRootCommitEventDetails) => void)Callback function that is fired when the value is committed. It runs later than `onValueChange`, when: - The input is blurred after typing a value. - The pointer is released after scrubbing or pressing the increment/decrement buttons. | - |
readOnly | booleanWhether the user should be unable to change the field value. | false |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, NumberFieldRootState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. | - |
required | booleanWhether the user must enter a value before submitting a form. | false |
size | "default" | "sm" | "lg"Height of the field and its steppers: `sm` 28px, `default` 32px, `lg` 36px, the heights of Input and Button. | - |
smallStep | numberThe small step value of the input element when incrementing while the alt key is held. Snaps to multiples of this value when `snapOnStep` is enabled. | 0.1 |
snapOnStep | booleanWhether the value should snap to the nearest step when incrementing or decrementing. | false |
step | number | "any"Amount to increment and decrement with the buttons and arrow keys, or to scrub with pointer movement in the scrub area. To always enable step validation on form submission, specify the `min` prop explicitly in conjunction with this prop. Specify `step="any"` to always disable step validation; interactive stepping then uses a base amount of `1`, while the alt and shift keys still step by `smallStep` and `largeStep`. | 1 |
value | number | nullThe raw numeric value of the field. | - |
NumberInputRoot
NumberInputRoot holds the value, range and step of a composed NumberInput, also exposed as NumberInput.Root. Every other part goes inside it.
| Prop | Type | Default |
|---|---|---|
allowOutOfRange | booleanWhen true, direct text entry may be outside the `min`/`max` range without clamping, so native range underflow/overflow validation can occur. Step-based interactions (keyboard arrows, buttons, wheel, scrub) still clamp. | false |
allowWheelScrub | booleanWhether to allow the user to scrub the input value with the mouse wheel while focused and hovering over the input. | false |
defaultValue | numberThe uncontrolled value of the field when it's initially rendered. | - |
disabled | booleanWhether the component should ignore user interaction. | false |
form | stringIdentifies the form that owns the hidden input. Useful when the number field is rendered outside the form. | - |
format | NumberFormatOptionsOptions to format the input value. | - |
id | stringThe id of the input element. | - |
inputRef | Ref<HTMLInputElement>A ref to access the hidden input element. Ref to the input element, to focus it from outside. | - |
largeStep | numberThe large step value of the input element when incrementing while the shift key is held. Snaps to multiples of this value when `snapOnStep` is enabled. | 10 |
locale | LocalesArgumentThe locale of the input element. Defaults to the user's runtime locale. | - |
max | numberThe maximum value of the input element. | - |
min | numberThe minimum value of the input element. | - |
name | stringIdentifies the field when a form is submitted. | - |
onValueChange | ((value: number | null, eventDetails: NumberFieldRootChangeEventDetails) => void)Callback fired when the number value changes. | - |
onValueCommitted | ((value: number | null, eventDetails: NumberFieldRootCommitEventDetails) => void)Callback function that is fired when the value is committed. It runs later than `onValueChange`, when: - The input is blurred after typing a value. - The pointer is released after scrubbing or pressing the increment/decrement buttons. | - |
readOnly | booleanWhether the user should be unable to change the field value. | false |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, NumberFieldRootState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. | - |
required | booleanWhether the user must enter a value before submitting a form. | false |
size | "default" | "sm" | "lg"Height of the field and its steppers: `sm` 28px, `default` 32px, `lg` 36px, the heights of Input and Button. | default |
smallStep | numberThe small step value of the input element when incrementing while the alt key is held. Snaps to multiples of this value when `snapOnStep` is enabled. | 0.1 |
snapOnStep | booleanWhether the value should snap to the nearest step when incrementing or decrementing. | false |
step | number | "any"Amount to increment and decrement with the buttons and arrow keys, or to scrub with pointer movement in the scrub area. To always enable step validation on form submission, specify the `min` prop explicitly in conjunction with this prop. Specify `step="any"` to always disable step validation; interactive stepping then uses a base amount of `1`, while the alt and shift keys still step by `smallStep` and `largeStep`. | 1 |
value | number | nullThe raw numeric value of the field. | - |
NumberInputGroup
NumberInputGroup joins the field and stepper buttons in one bordered box, as tall as an Input of the same size.
| Prop | Type | Default |
|---|---|---|
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, NumberFieldGroupState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. | - |
NumberInputField
NumberInputField is the text input of a composed NumberInput.
| Prop | Type | Default |
|---|---|---|
aria-roledescription | stringA user-friendly description of the input's role for assistive tech. This is a role description, not an accessible name — use `Field.Label` or `aria-label` to name the control. | 'Number field' |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, NumberFieldInputState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. | - |
NumberInputIncrement
NumberInputIncrement is the plus button. Its accessible name defaults to the English "Increase"; pass a translated aria-label.
| Prop | Type | Default |
|---|---|---|
nativeButton | booleanWhether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). | true |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, NumberFieldIncrementState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. | - |
NumberInputDecrement
NumberInputDecrement is the minus button. Its accessible name defaults to the English "Decrease"; pass a translated aria-label.
| Prop | Type | Default |
|---|---|---|
nativeButton | booleanWhether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). | true |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, NumberFieldDecrementState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. | - |
NumberInputScrubArea
NumberInputScrubArea changes the value as the pointer drags across it, such as on the field's label.
| Prop | Type | Default |
|---|---|---|
direction | "horizontal" | "vertical"Cursor movement direction in the scrub area. | 'horizontal' |
pixelSensitivity | numberDetermines how many pixels the cursor must move before the value changes. A higher value will make scrubbing less sensitive. | 2 |
render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, NumberFieldScrubAreaState>Allows you to replace the component's HTML element with a different tag, or compose it with another component. | - |
teleportDistance | numberIf specified, determines the distance that the cursor may move from the center of the scrub area before it will loop back around. | - |