Number Input
The number input provides controls for editing, incrementing or decrementing numeric values using the keyboard or pointer.
Features
- Based on the spinbutton pattern
- Supports using the scroll wheel to increment and decrement the value
- Handles floating point rounding errors when incrementing, decrementing, and snapping to step
- Supports pressing and holding the spin buttons to continuously increment or decrement
- Supports rounding value to specific number of fraction digits
- Supports scrubbing interaction
Installation
Install the number input package:
npm install @zag-js/number-input @zag-js/react # or yarn add @zag-js/number-input @zag-js/react
npm install @zag-js/number-input @zag-js/solid # or yarn add @zag-js/number-input @zag-js/solid
npm install @zag-js/number-input @zag-js/vue # or yarn add @zag-js/number-input @zag-js/vue
npm install @zag-js/number-input @zag-js/svelte # or yarn add @zag-js/number-input @zag-js/svelte
Anatomy
To set up the number input correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the number input package:
import * as numberInput from "@zag-js/number-input"
The number input package exports two key functions:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
Pass a unique
idtouseMachineso generated element ids stay predictable.
Then use the framework integration helpers:
import * as numberInput from "@zag-js/number-input" import { useMachine, normalizeProps } from "@zag-js/react" import { useId } from "react" export function NumberInput() { const service = useMachine(numberInput.machine, { id: useId() }) const api = numberInput.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Enter number:</label> <div> <button {...api.getDecrementTriggerProps()}>DEC</button> <input {...api.getInputProps()} /> <button {...api.getIncrementTriggerProps()}>INC</button> </div> </div> ) }
import * as numberInput from "@zag-js/number-input" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" export function NumberInput() { const service = useMachine(numberInput.machine, { id: createUniqueId() }) const api = createMemo(() => numberInput.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> <label {...api().getLabelProps()}>Enter number:</label> <div> <button {...api().getDecrementTriggerProps()}>DEC</button> <input {...api().getInputProps()} /> <button {...api().getIncrementTriggerProps()}>INC</button> </div> </div> ) }
<script setup> import * as numberInput from "@zag-js/number-input" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, useId } from "vue" const service = useMachine(numberInput.machine, { id: useId() }) const api = computed(() => numberInput.connect(service, normalizeProps)) </script> <template> <div ref="ref" v-bind="api.getRootProps()"> <label v-bind="api.getLabelProps()">Enter number</label> <div> <button v-bind="api.getDecrementTriggerProps()">DEC</button> <input v-bind="api.getInputProps()" /> <button v-bind="api.getIncrementTriggerProps()">INC</button> </div> </div> </template>
<script lang="ts"> import * as numberInput from "@zag-js/number-input" import { normalizeProps, useMachine } from "@zag-js/svelte" const id = $props.id() const service = useMachine(numberInput.machine, { id }) const api = $derived(numberInput.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Enter number:</label> <div> <button {...api.getDecrementTriggerProps()}>DEC</button> <input {...api.getInputProps()} /> <button {...api.getIncrementTriggerProps()}>INC</button> </div> </div>
Setting the initial value
Set defaultValue to define the initial value. The value must be a string.
const service = useMachine(numberInput.machine, { defaultValue: "13", })
Controlled value
Use value and onValueChange to control the value externally.
Note: Since the value can be formatted, it's important to preserve the value as a string.
import { useState } from "react" export function ControlledNumberInput() { const [value, setValue] = useState("") const service = useMachine(numberInput.machine, { value, onValueChange(details) { setValue(details.value) }, }) }
import { createSignal } from "solid-js" export function ControlledNumberInput() { const [value, setValue] = createSignal("") const service = useMachine(numberInput.machine, { get value() { return value() }, onValueChange(details) { setValue(details.value) }, }) return ( // ... ) }
<script setup lang="ts"> import { ref } from "vue" const valueRef = ref("") const service = useMachine(numberInput.machine, { get value() { return valueRef.value }, onValueChange(details) { valueRef.value = details.value }, }) </script>
<script lang="ts"> let value = $state("") const service = useMachine(numberInput.machine, { get value() { return value }, onValueChange(details) { value = details.value }, }) </script>
Setting a minimum and maximum value
Pass the min prop or max prop to set an upper and lower limit for the input.
By default, the input will restrict the value to stay within the specified
range.
const service = useMachine(numberInput.machine, { min: 10, max: 200, })
To allow the value overflow the specified min or max, set the
allowOverflow: truein the context.
Validating overflow and underflow
Use onValueInvalid to react when the value goes below min or above max.
const service = useMachine(numberInput.machine, { min: 0, max: 10, allowOverflow: true, onValueInvalid(details) { // details => { value: string, valueAsNumber: number, reason: "rangeUnderflow" | "rangeOverflow" } console.log(details.reason) }, })
Scrubbing the input value
Number input supports the scrubber interaction pattern. To use this pattern,
spread api.getScrubberProps() on the scrubbing element.
It uses the Pointer lock API and tracks the pointer movement. It also renders a virtual cursor which mimics the real cursor's pointer.
import * as numberInput from "@zag-js/number-input" import { useMachine, normalizeProps } from "@zag-js/react" export function NumberInput() { const service = useMachine(numberInput.machine, { id: "1" }) const api = numberInput.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Enter number:</label> <div> <div {...api.getScrubberProps()} /> <input {...api.getInputProps()} /> </div> </div> ) }
import * as numberInput from "@zag-js/number-input" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId } from "solid-js" export function NumberInput() { const service = useMachine(numberInput.machine, { id: createUniqueId() }) const api = createMemo(() => numberInput.connect(service, normalizeProps)) return ( <div {...api().getRootProps()}> <label {...api().getLabelProps()}>Enter number:</label> <div> <div {...api().getScrubberProps()} /> <input {...api().getInputProps()} /> </div> </div> ) }
<script setup> import * as numberInput from "@zag-js/number-input" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed } from "vue" const service = useMachine(numberInput.machine, { id: "1" }) const api = computed(() => numberInput.connect(service, normalizeProps)) </script> <template> <div ref="ref" v-bind="api.getRootProps()"> <label v-bind="api.getLabelProps()">Enter number</label> <div> <div v-bind="api.getScrubberProps()" /> <input v-bind="api.getInputProps()" /> </div> </div> </template>
<script lang="ts"> import * as numberInput from "@zag-js/number-input" import { normalizeProps, useMachine } from "@zag-js/svelte" const id = $props.id() const service = useMachine(numberInput.machine, { id }) const api = $derived(numberInput.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> <label {...api.getLabelProps()}>Enter number:</label> <div> <div {...api.getScrubberProps()}></div> <input {...api.getInputProps()} /> </div> </div>
Using the mousewheel to change value
The number input machine exposes a way to increment/decrement the value using
the mouse wheel event. To activate this, pass the allowMouseWheel property to
the machine's context.
const service = useMachine(numberInput.machine, { allowMouseWheel: true, })
Clamp value when user blurs the input
In most cases, users can type custom values in the input field. If the typed value is greater than the max, the value is reset to max when the user blurs the input.
To disable this behavior, pass clampValueOnBlur and set to false.
const service = useMachine(numberInput.machine, { clampValueOnBlur: false, })
Listening for value changes
When the value changes, the onValueChange callback is invoked.
const service = useMachine(numberInput.machine, { onValueChange(details) { // details => { value: string, valueAsNumber: number } console.log("value is:", details.value) }, })
Listening for value commit
Use onValueCommit to react when the value is committed (blur or Enter).
const service = useMachine(numberInput.machine, { onValueCommit(details) { // details => { value: string, valueAsNumber: number } console.log("committed:", details.value) }, })
Listening for focus changes
Use onFocusChange to react to focus and blur transitions.
const service = useMachine(numberInput.machine, { onFocusChange(details) { // details => { focused: boolean, value: string, valueAsNumber: number } console.log("focused:", details.focused) }, })
Usage within forms
To use the number input within forms, set the name property in the machine's
context.
const service = useMachine(numberInput.machine, { name: "quantity", })
Adjusting the precision of the value
To format the input value to be rounded to specific decimal points, set the
formatOptions and provide Intl.NumberFormatOptions such as
maximumFractionDigits or minimumFractionDigits
const service = useMachine(numberInput.machine, { formatOptions: { maximumFractionDigits: 4, minimumFractionDigits: 2, }, })
Disabling long press spin
To disable the long press spin, set the spinOnPress to false.
const service = useMachine(numberInput.machine, { spinOnPress: false, })
Choosing mobile keyboard type
Set inputMode to hint the keyboard type on mobile.
const service = useMachine(numberInput.machine, { inputMode: "numeric", // "text" | "tel" | "numeric" | "decimal" })
Format and parse value
To apply custom formatting to the input's value, set the formatOptions and
provide Intl.NumberFormatOptions such as style and currency
const service = useMachine(numberInput.machine, { formatOptions: { style: "currency", currency: "USD", }, })
Customizing accessibility labels
Use translations to customize increment/decrement labels and value text.
const service = useMachine(numberInput.machine, { translations: { incrementLabel: "Increase quantity", decrementLabel: "Decrease quantity", valueText: (value) => `${value} units`, }, })
Submitting with an external form
Set form if the hidden input should submit with a form outside the current DOM
subtree.
const service = useMachine(numberInput.machine, { name: "value", form: "checkout-form", })
Styling guide
Each part includes a data-part attribute you can target in CSS.
Disabled state
When the number input is disabled, the root, label and input parts will have
data-disabled attribute added to them.
The increment and decrement spin buttons are disabled when the number input is disabled and the min/max is reached.
[data-part="root"][data-disabled] { /* disabled styles for the input */ } [data-part="input"][data-disabled] { /* disabled styles for the input */ } [data-part="label"][data-disabled] { /* disabled styles for the label */ } [data-part="increment-trigger"][data-disabled] { /* disabled styles for the increment button */ } [data-part="decrement-trigger"][data-disabled] { /* disabled styles for the decrement button */ }
Invalid state
The number input is invalid, either by passing invalid: true or when the value
exceeds the max and allowOverflow: true is passed. When this happens, the
root, label and input parts will have data-invalid attribute added to them.
[data-part="root"][data-invalid] { /* disabled styles for the input */ } [data-part="input"][data-invalid] { /* invalid styles for the input */ } [data-part="label"][data-invalid] { /* invalid styles for the label */ }
Readonly state
When the number input is readonly, the input part will have data-readonly
added.
[data-part="input"][data-readonly] { /* readonly styles for the input */ }
Increment and decrement spin buttons
The spin buttons can be styled individually with their respective data-part
attribute.
[data-part="increment-trigger"] { /* styles for the increment trigger element */ } [data-part="decrement-trigger"] { /* styles for the decrement trigger element */ }
Methods and Properties
Machine Context
The number input machine exposes the following context properties:
idsPartial<{ root: string; label: string; input: string; incrementTrigger: string; decrementTrigger: string; scrubber: string; }>The ids of the elements in the number input. Useful for composition.namestringThe name attribute of the number input. Useful for form submission.formstringThe associate form of the input element.disabledbooleanWhether the number input is disabled.readOnlybooleanWhether the number input is readonlyinvalidbooleanWhether the number input value is invalid.requiredbooleanWhether the number input is requiredpatternstringThe pattern used to check the <input> element's value againstvaluestringThe controlled value of the inputdefaultValuestringThe initial value of the input when rendered. Use when you don't need to control the value of the input.minnumberThe minimum value of the number inputmaxnumberThe maximum value of the number inputstepnumberThe amount to increment or decrement the value byallowMouseWheelbooleanWhether to allow mouse wheel to change the valueallowOverflowbooleanWhether to allow the value overflow the min/max rangeclampValueOnBlurbooleanWhether to clamp the value when the input loses focus (blur)focusInputOnChangebooleanWhether to focus input when the value changestranslationsIntlTranslationsSpecifies the localized strings that identifies the accessibility elements and their statesformatOptionsIntl.NumberFormatOptionsThe options to pass to the `Intl.NumberFormat` constructorinputModeInputModeHints at the type of data that might be entered by the user. It also determines the type of keyboard shown to the user on mobile devicesonValueChange(details: ValueChangeDetails) => voidFunction invoked when the value changesonValueInvalid(details: ValueInvalidDetails) => voidFunction invoked when the value overflows or underflows the min/max rangeonFocusChange(details: FocusChangeDetails) => voidFunction invoked when the number input is focusedonValueCommit(details: ValueChangeDetails) => voidFunction invoked when the value is committed (when the input is blurred or the Enter key is pressed)spinOnPressbooleanWhether to spin the value when the increment/decrement button is pressedlocalestringThe current locale. Based on the BCP 47 definition.dir"ltr" | "rtl"The document's text/writing direction.idstringThe unique identifier of the machine.getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The number input api exposes the following methods:
focusedbooleanWhether the input is focused.invalidbooleanWhether the input is invalid.emptybooleanWhether the input value is empty.valuestringThe formatted value of the input.valueAsNumbernumberThe value of the input as a number.setValue(value: number) => voidFunction to set the value of the input.clearValueVoidFunctionFunction to clear the value of the input.incrementVoidFunctionFunction to increment the value of the input by the step.decrementVoidFunctionFunction to decrement the value of the input by the step.setToMaxVoidFunctionFunction to set the value of the input to the max.setToMinVoidFunctionFunction to set the value of the input to the min.focusVoidFunctionFunction to focus the input.
Data Attributes
Accessibility
Keyboard Interactions
- ArrowUpIncrements the value of the number input by a predefined step.
- ArrowDownDecrements the value of the number input by a predefined step.
- PageUpIncrements the value of the number input by a larger predefined step.
- PageDownDecrements the value of the number input by a larger predefined step.
- HomeSets the value of the number input to its minimum allowed value.
- EndSets the value of the number input to its maximum allowed value.
- EnterSubmits the value entered in the number input.