Skip to main content
The number input provides controls for editing, incrementing, or decrementing numeric values using the keyboard or pointer. It handles floating-point rounding, scrubbing interaction, and value formatting via Intl.NumberFormat. Features
  • Based on the spinbutton ARIA pattern
  • Scroll wheel, pointer scrubbing, and keyboard increment/decrement
  • Handles floating-point rounding errors with step snapping
  • Press-and-hold spin buttons for continuous increment/decrement
  • Rounds value to specific fraction digit counts via formatOptions
  • Currency, percent, and custom number formatting

Installation

npm install @zag-js/number-input @zag-js/react

Usage

import { useMachine, normalizeProps } from "@zag-js/react"
import * as numberInput from "@zag-js/number-input"
import { useId } from "react"

export function NumberInput() {
  const service = useMachine(numberInput.machine, {
    id: useId(),
    defaultValue: "0",
    min: 0,
    max: 100,
  })
  const api = numberInput.connect(service, normalizeProps)

  return (
    <div {...api.getRootProps()}>
      <label {...api.getLabelProps()}>Quantity</label>
      <div>
        <button {...api.getDecrementTriggerProps()}>-</button>
        <input {...api.getInputProps()} />
        <button {...api.getIncrementTriggerProps()}>+</button>
      </div>
    </div>
  )
}

Setting the initial value

Pass defaultValue to set an uncontrolled initial value. The value must be a string.
const service = useMachine(numberInput.machine, {
  id: useId(),
  defaultValue: "13",
})

Controlled value

Use value and onValueChange to control the value externally.
const [value, setValue] = useState("0")

const service = useMachine(numberInput.machine, {
  id: useId(),
  value,
  onValueChange(details) {
    // details => { value: string, valueAsNumber: number }
    setValue(details.value)
  },
})

Min, max, and overflow

Set min and max to constrain the range. By default the value clamps to stay within bounds.
const service = useMachine(numberInput.machine, {
  id: useId(),
  min: 10,
  max: 200,
})
To allow the value to exceed the range (and use onValueInvalid for feedback), pass allowOverflow: true.
const service = useMachine(numberInput.machine, {
  id: useId(),
  min: 0,
  max: 10,
  allowOverflow: true,
  onValueInvalid(details) {
    // details => { value, valueAsNumber, reason: "rangeUnderflow" | "rangeOverflow" }
    console.log(details.reason)
  },
})

Scroll wheel increment

Activate scroll-wheel increment/decrement by setting allowMouseWheel: true.
const service = useMachine(numberInput.machine, {
  id: useId(),
  allowMouseWheel: true,
})

Clamp on blur

By default, values are clamped to min/max when the input loses focus. Disable this with clampValueOnBlur: false.
const service = useMachine(numberInput.machine, {
  id: useId(),
  clampValueOnBlur: false,
})

Number formatting

Pass standard Intl.NumberFormatOptions to formatOptions to control display and precision.
// Currency
const service = useMachine(numberInput.machine, {
  id: useId(),
  formatOptions: {
    style: "currency",
    currency: "USD",
  },
})

// Decimal precision
const service = useMachine(numberInput.machine, {
  id: useId(),
  formatOptions: {
    maximumFractionDigits: 4,
    minimumFractionDigits: 2,
  },
})

Usage within forms

Set name to include the value in form submissions.
const service = useMachine(numberInput.machine, {
  id: useId(),
  name: "quantity",
})

Accessibility labels

Use translations to customize increment/decrement button labels and value text for screen readers.
const service = useMachine(numberInput.machine, {
  id: useId(),
  translations: {
    incrementLabel: "Increase quantity",
    decrementLabel: "Decrease quantity",
    valueText: (value) => `${value} units`,
  },
})

API reference

defaultValue
string
The initial value of the number input (uncontrolled).
value
string
The controlled value of the number input. Use with onValueChange.
min
number
The minimum allowed value.
max
number
The maximum allowed value.
step
number
The step amount for increment/decrement. Defaults to 1.
allowOverflow
boolean
Whether to allow the value to exceed min/max. Defaults to false.
clampValueOnBlur
boolean
Whether to clamp the value to the range on blur. Defaults to true.
allowMouseWheel
boolean
Whether to increment/decrement with the scroll wheel.
spinOnPress
boolean
Whether to continuously spin value while the spin button is pressed. Defaults to true.
formatOptions
Intl.NumberFormatOptions
Options passed to Intl.NumberFormat for display formatting and parsing.
inputMode
"text" | "tel" | "numeric" | "decimal"
The virtual keyboard type to show on mobile.
name
string
The name attribute of the hidden input used in form submissions.
disabled
boolean
Whether the number input is disabled.
readOnly
boolean
Whether the number input is read-only.
invalid
boolean
Whether the number input is in an invalid state.
onValueChange
(details: { value: string, valueAsNumber: number }) => void
Callback fired whenever the value changes.
onValueCommit
(details: { value: string, valueAsNumber: number }) => void
Callback fired when the value is committed (on blur or Enter key).
onValueInvalid
(details: { value: string, valueAsNumber: number, reason: string }) => void
Callback fired when the value falls outside the allowed range.
translations
IntlTranslations
Localized labels for increment/decrement triggers and value text.

Styling

Each part of the number input has a data-part attribute for CSS targeting.

Part names

[data-part="root"] { /* wrapper element */ }
[data-part="label"] { /* the label */ }
[data-part="input"] { /* the text input */ }
[data-part="increment-trigger"] { /* the + button */ }
[data-part="decrement-trigger"] { /* the - button */ }
[data-part="scrubber"] { /* scrub interaction element */ }

Disabled state

[data-part="root"][data-disabled] { /* disabled root */ }
[data-part="label"][data-disabled] { /* disabled label */ }
[data-part="input"][data-disabled] { /* disabled input */ }
[data-part="increment-trigger"][data-disabled] { /* disabled + button */ }
[data-part="decrement-trigger"][data-disabled] { /* disabled - button */ }

Invalid state

[data-part="root"][data-invalid] { /* invalid root */ }
[data-part="label"][data-invalid] { /* invalid label */ }
[data-part="input"][data-invalid] { /* invalid input */ }

Read-only state

[data-part="input"][data-readonly] { /* read-only input */ }

Keyboard interactions

KeyDescription
ArrowUpIncrements the value by the step amount
ArrowDownDecrements the value by the step amount
PageUpIncrements the value by a larger step (10× step)
PageDownDecrements the value by a larger step (10× step)
HomeSets the value to the min
EndSets the value to the max
EnterCommits the current typed value

Build docs developers (and LLMs) love