Skip to content
UI

Number Input

A text field for a number, with keyboard stepping, optional increment and decrement buttons, and locale-aware formatting.

@zenoxai/ui/components/number-inputBase UI Markdown
<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 with ariaLabel, or with id and a FieldLabel. A plain aria-label on NumberInput lands on the wrapper, not on the input.
  • ArrowUp and ArrowDown change the value by step, with Shift by largeStep (10 by default) and with Alt by smallStep (0.1 by default). Home and End jump to min and max.
  • The value is clamped to min and max when 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:

PropTypeDefault
allowOutOfRangeboolean

When 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
allowWheelScrubboolean

Whether to allow the user to scrub the input value with the mouse wheel while focused and hovering over the input.

false
ariaLabelstring

Accessible name of the input, when no visible label points at it.

-
defaultValuenumber

The uncontrolled value of the field when it's initially rendered.

-
disabledboolean

Whether the component should ignore user interaction.

false
formstring

Identifies the form that owns the hidden input. Useful when the number field is rendered outside the form.

-
formatNumberFormatOptions

Options to format the input value.

-
idstring

The id of the input element.

-
inputClassNamestring

Classes for the input; `className` styles the root.

-
inputRefRef<HTMLInputElement>

A ref to access the hidden input element. Ref to the input element, to focus it from outside.

-
largeStepnumber

The 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
localeLocalesArgument

The locale of the input element. Defaults to the user's runtime locale.

-
maxnumber

The maximum value of the input element.

-
minnumber

The minimum value of the input element.

-
namestring

Identifies 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.

-
readOnlyboolean

Whether the user should be unable to change the field value.

false
renderReactElement<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.

-
requiredboolean

Whether 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.

-
smallStepnumber

The 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
snapOnStepboolean

Whether the value should snap to the nearest step when incrementing or decrementing.

false
stepnumber | "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
valuenumber | null

The 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.

PropTypeDefault
allowOutOfRangeboolean

When 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
allowWheelScrubboolean

Whether to allow the user to scrub the input value with the mouse wheel while focused and hovering over the input.

false
defaultValuenumber

The uncontrolled value of the field when it's initially rendered.

-
disabledboolean

Whether the component should ignore user interaction.

false
formstring

Identifies the form that owns the hidden input. Useful when the number field is rendered outside the form.

-
formatNumberFormatOptions

Options to format the input value.

-
idstring

The id of the input element.

-
inputRefRef<HTMLInputElement>

A ref to access the hidden input element. Ref to the input element, to focus it from outside.

-
largeStepnumber

The 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
localeLocalesArgument

The locale of the input element. Defaults to the user's runtime locale.

-
maxnumber

The maximum value of the input element.

-
minnumber

The minimum value of the input element.

-
namestring

Identifies 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.

-
readOnlyboolean

Whether the user should be unable to change the field value.

false
renderReactElement<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.

-
requiredboolean

Whether 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
smallStepnumber

The 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
snapOnStepboolean

Whether the value should snap to the nearest step when incrementing or decrementing.

false
stepnumber | "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
valuenumber | null

The 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.

PropTypeDefault
renderReactElement<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.

PropTypeDefault
aria-roledescriptionstring

A 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'
renderReactElement<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.

PropTypeDefault
nativeButtonboolean

Whether 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
renderReactElement<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.

PropTypeDefault
nativeButtonboolean

Whether 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
renderReactElement<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.

PropTypeDefault
direction"horizontal" | "vertical"

Cursor movement direction in the scrub area.

'horizontal'
pixelSensitivitynumber

Determines how many pixels the cursor must move before the value changes. A higher value will make scrubbing less sensitive.

2
renderReactElement<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.

-
teleportDistancenumber

If specified, determines the distance that the cursor may move from the center of the scrub area before it will loop back around.

-