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.
- Support for scrubbing interaction.
Installation
To use the number input machine in your project, run the following command in your command line:
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/vue # or yarn add @zag-js/number-input @zag-js/vue
This command will install the framework agnostic number input logic and the reactive utilities for your framework of choice.
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-part
attribute to help identify them in the DOM.
Usage
First, import the number input package into your project
import * as numberInput from "@zag-js/number-input"
The number input package exports two key functions:
machine
— The state machine logic for the number input widget as described in the WAI-ARIA spinner pattern.connect
— The function that translates the machine's state to JSX attributes and event handlers.
You'll need to provide a unique
id
to theuseMachine
hook. This is used to ensure that the every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the number input machine in your project 🔥
import * as numberInput from "@zag-js/number-input" import { useMachine, normalizeProps } from "@zag-js/react" export function NumberInput() { const [state, send] = useMachine(numberInput.machine({ id: "1" })) const api = numberInput.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> <label {...api.labelProps}>Enter number:</label> <div> <button {...api.decrementTriggerProps}>DEC</button> <input {...api.inputProps} /> <button {...api.incrementTriggerProps}>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 [state, send] = useMachine(numberInput.machine({ id: createUniqueId() })) const api = createMemo(() => numberInput.connect(state, send, normalizeProps)) return ( <div {...api().rootProps}> <label {...api().labelProps}>Enter number:</label> <div> <button {...api().decrementTriggerProps}>DEC</button> <input {...api().inputProps} /> <button {...api().incrementTriggerProps}>INC</button> </div> </div> ) }
import * as numberInput from "@zag-js/number-input" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, defineComponent, h, Fragment } from "vue" export default defineComponent({ name: "NumberInput", setup() { const [state, send] = useMachine(numberInput.machine({ id: "1" })) const apiRef = computed(() => numberInput.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <div {...api.rootProps}> <label {...api.labelProps}>Enter number</label> <div> <button {...api.decrementTriggerProps}>DEC</button> <input {...api.inputProps} /> <button {...api.incrementTriggerProps}>INC</button> </div> </div> ) } }, })
<script setup> import * as numberInput from "@zag-js/number-input"; import { normalizeProps, useMachine } from "@zag-js/vue"; import { computed } from "vue"; const [state, send] = useMachine(numberInput.machine({ id: "1" })); const api = computed(() => numberInput.connect(state.value, send, normalizeProps) ); </script> <template> <div ref="ref" v-bind="api.rootProps"> <label v-bind="api.labelProps">Enter number</label> <div> <button v-bind="api.decrementTriggerProps">DEC</button> <input v-bind="api.inputProps" /> <button v-bind="api.incrementTriggerProps">INC</button> </div> </div> </template>
Setting the initial value
To set the initial value of the number input, you can set the value
context
property.
Note: The value must be a string
const [state, send] = useMachine( numberInput.machine({ value: "13", }), )
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 [state, send] = useMachine( numberInput.machine({ min: 10, max: 200, }), )
To allow the value overflow the specified min or max, set the
allowOverflow: true
in the context.
Scrubbing the input value
The number input machine supports the scrubber interaction pattern. The use this
pattern, spread the scrubberProps
from the api
on to the scrubbing element.
It uses the Pointer lock API and tracks the pointer movement. It also renders a virtual cursor which mimicks the real cursor's pointer.
import * as numberInput from "@zag-js/number-input" import { useMachine, normalizeProps } from "@zag-js/react" export function NumberInput() { const [state, send] = useMachine(numberInput.machine({ id: "1" })) const api = numberInput.connect(state, send, normalizeProps) return ( <div {...api.rootProps}> <label {...api.labelProps}>Enter number:</label> <div> <div {...api.scrubberProps} /> <input {...api.inputProps} /> </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 [state, send] = useMachine(numberInput.machine({ id: createUniqueId() })) const api = createMemo(() => numberInput.connect(state, send, normalizeProps)) return ( <div {...api().rootProps}> <label {...api().labelProps}>Enter number:</label> <div> <div {...api.scrubberProps} /> <input {...api().inputProps} /> </div> </div> ) }
import * as numberInput from "@zag-js/number-input" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed, defineComponent, h, Fragment } from "vue" export default defineComponent({ name: "NumberInput", setup() { const [state, send] = useMachine(numberInput.machine({ id: "1" })) const apiRef = computed(() => numberInput.connect(state.value, send, normalizeProps), ) return () => { const api = apiRef.value return ( <div {...api.rootProps}> <label {...api.labelProps}>Enter number</label> <div> <div {...api.scrubberProps} /> <input {...api.inputProps} /> </div> </div> ) } }, })
<script setup> import * as numberInput from "@zag-js/number-input"; import { normalizeProps, useMachine } from "@zag-js/vue"; import { computed } from "vue"; const [state, send] = useMachine(numberInput.machine({ id: "1" })); const api = computed(() => numberInput.connect(state.value, send, normalizeProps) ); </script> <template> <div ref="ref" v-bind="api.rootProps"> <label v-bind="api.labelProps">Enter number</label> <div> <div v-bind="api.scrubberProps" /> <input v-bind="api.inputProps" /> </div> </div> </template>
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 [state, send] = 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 blur out of the input.
To disable this behavior, pass clampValueOnBlur
and set to false
.
const [state, send] = useMachine( numberInput.machine({ clampValueOnBlur: false, }), )
Listening for value changes
When the value changes, the onValueChange
callback is invoked.
const [state, send] = useMachine( numberInput.machine({ onValueChange(details) { // details => { value: string, valueAsNumber: number } console.log("value is:", details.value) }, }), )
Usage within forms
To use the number input within forms, set the name
property in the machine's
context.
const [state, send] = 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 [state, send] = useMachine( numberInput.machine({ formatOptions: { maximumFractionDigits: 4, minimumFractionDigits: 2, }, }), )
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 [state, send] = useMachine( numberInput.machine({ formatOptions: { style: "currency", currency: "USD", }, }), )
Styling guide
Earlier, we mentioned that each number-input's part has a data-part
attribute
added to them to select and style them in the DOM.
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:
ids
Partial<{ root: string; label: string; input: string; incrementTrigger: string; decrementTrigger: string; scrubber: string; }>
The ids of the elements in the number input. Useful for composition.name
string
The name attribute of the number input. Useful for form submission.form
string
The associate form of the input element.disabled
boolean
Whether the number input is disabled.readOnly
boolean
Whether the number input is readonlyinvalid
boolean
Whether the number input value is invalid.pattern
string
The pattern used to check the <input> element's value againstvalue
string
The value of the inputmin
number
The minimum value of the number inputmax
number
The maximum value of the number inputstep
number
The amount to increment or decrement the value byallowMouseWheel
boolean
Whether to allow mouse wheel to change the valueallowOverflow
boolean
Whether to allow the value overflow the min/max rangeclampValueOnBlur
boolean
Whether to clamp the value when the input loses focus (blur)focusInputOnChange
boolean
Whether to focus input when the value changestranslations
IntlTranslations
Specifies the localized strings that identifies the accessibility elements and their statesformatOptions
Intl.NumberFormatOptions
The options to pass to the `Intl.NumberFormat` constructorinputMode
InputMode
Hints 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) => void
Function invoked when the value changesonValueInvalid
(details: ValueInvalidDetails) => void
Function invoked when the value overflows or underflows the min/max rangeonFocusChange
(details: FocusChangeDetails) => void
Function invoked when the number input is focusedspinOnPress
boolean
Whether to spin the value when the increment/decrement button is pressedlocale
string
The current locale. Based on the BCP 47 definition.dir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The number input api
exposes the following methods:
isFocused
boolean
Whether the input is focused.isInvalid
boolean
Whether the input is invalid.isValueEmpty
boolean
Whether the input value is empty.value
string
The formatted value of the input.valueAsNumber
number
The value of the input as a number.setValue
(value: number) => void
Function to set the value of the input.clearValue
() => void
Function to clear the value of the input.increment
() => void
Function to increment the value of the input by the step.decrement
() => void
Function to decrement the value of the input by the step.setToMax
() => void
Function to set the value of the input to the max.setToMin
() => void
Function to set the value of the input to the min.focus
() => void
Function to focus the input.
Edit this page on GitHub