Skip to content
Destyler UI Destyler UI Destyler UI

Number Input

The number input provides controls for editing, incrementing or decrementing numeric values using the keyboard or pointer.

Features

Install

Install the component from your command line.

Terminal window
      
        
npm install @destyler/number-input @destyler/vue
Terminal window
      
        
npm install @destyler/number-input @destyler/react
Terminal window
      
        
npm install @destyler/number-input @destyler/svelte
Terminal window
      
        
npm install @destyler/number-input @destyler/solid

Anatomy

Import all parts and piece them together.

<script setup lang="ts">
import * as numberInput from '@destyler/number-input'
import { normalizeProps, useMachine } from '@destyler/vue'
import { computed, useId } from 'vue'
const [state, send] = useMachine(numberInput.machine({
id: useId(),
}))
const api = computed(() => numberInput.connect(state.value, send, normalizeProps))
</script>
<template>
<div v-bind="api.getRootProps()" >
<label v-bind="api.getLabelProps()"></label>
<button v-bind="api.getDecrementTriggerProps()" />
<input v-bind="api.getInputProps()">
<button v-bind="api.getIncrementTriggerProps()" />
</div>
</template>
import * as numberInput from '@destyler/number-input'
import { normalizeProps, useMachine } from '@destyler/react'
import { useId } from 'react'
export default function NumberInput() {
const [state, send] = useMachine(numberInput.machine({
id: useId(),
}))
const api = numberInput.connect(state, send, normalizeProps)
return (
<div {...api.getRootProps()}>
<label {...api.getLabelProps()}></label>
<button {...api.getDecrementTriggerProps()} ></button>
<input {...api.getInputProps()}/>
<button {...api.getIncrementTriggerProps()}></button>
</div>
)
}
<script lang="ts">
import * as numberInput from '@destyler/number-input'
import { normalizeProps, useMachine } from '@destyler/svelte'
const id = $props.id()
const [state, send] = useMachine(numberInput.machine({
id,
}))
const api = $derived(numberInput.connect(state, send, normalizeProps))
</script>
<div {...api.getRootProps()}>
<label {...api.getLabelProps()}></label>
<button {...api.getDecrementTriggerProps()}></button>
<input {...api.getInputProps()} />
<button {...api.getIncrementTriggerProps()}></button>
</div>
import * as numberInput from '@destyler/number-input'
import { normalizeProps, useMachine } from '@destyler/solid'
import { createMemo, createUniqueId } from 'solid-js'
export default function NumberInput() {
const [state, send] = useMachine(numberInput.machine({
id: createUniqueId(),
}))
const api = createMemo(() => numberInput.connect(state, send, normalizeProps))
return (
<div {...api().getRootProps()}>
<label {...api().getLabelProps()}></label>
<button {...api().getDecrementTriggerProps()}></button>
<input {...api().getInputProps()}/>
<button {...api().getIncrementTriggerProps()} ></button>
</div>
)
}

Setting the initial value

To set the initial value of the number input, you can set the value context property.

const [state, send] = useMachine(
numberInput.machine({
value: "66",
}),
)

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: 0,
max: 100,
}),
)

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 mimics the real cursor’s pointer.

<script setup lang="ts">
import * as numberInput from '@destyler/number-input'
import { normalizeProps, useMachine } from '@destyler/vue'
import { computed, useId } from 'vue'
const [state, send] = useMachine(numberInput.machine({
id: useId(),
}))
const api = computed(() => numberInput.connect(state.value, send, normalizeProps))
</script>
<template>
<div v-bind="api.getRootProps()" >
<label v-bind="api.getLabelProps()"></label>
<button v-bind="api.getDecrementTriggerProps()" />
<div v-bind="api.getScrubberProps()" />
<input v-bind="api.getInputProps()">
<button v-bind="api.getIncrementTriggerProps()" />
</div>
</template>
import * as numberInput from '@destyler/number-input'
import { normalizeProps, useMachine } from '@destyler/react'
import { useId } from 'react'
export default function NumberInput() {
const [state, send] = useMachine(numberInput.machine({
id: useId(),
}))
const api = numberInput.connect(state, send, normalizeProps)
return (
<div {...api.getRootProps()}>
<label {...api.getLabelProps()}></label>
<button {...api.getDecrementTriggerProps()} ></button>
<div {...api.getScrubberProps()} />
<input {...api.getInputProps()}/>
<button {...api.getIncrementTriggerProps()}></button>
</div>
)
}
<script lang="ts">
import * as numberInput from '@destyler/number-input'
import { normalizeProps, useMachine } from '@destyler/svelte'
const id = $props.id()
const [state, send] = useMachine(numberInput.machine({
id,
}))
const api = $derived(numberInput.connect(state, send, normalizeProps))
</script>
<div {...api.getRootProps()}>
<label {...api.getLabelProps()}></label>
<button {...api.getDecrementTriggerProps()}></button>
<div {...api.getScrubberProps()}></div>
<input {...api.getInputProps()} />
<button {...api.getIncrementTriggerProps()}></button>
</div>
import * as numberInput from '@destyler/number-input'
import { normalizeProps, useMachine } from '@destyler/solid'
import { createMemo, createUniqueId } from 'solid-js'
export default function NumberInput() {
const [state, send] = useMachine(numberInput.machine({
id: createUniqueId(),
}))
const api = createMemo(() => numberInput.connect(state, send, normalizeProps))
return (
<div {...api().getRootProps()}>
<label {...api().getLabelProps()}></label>
<button {...api().getDecrementTriggerProps()}></button>
<div {...api().getScrubberProps()} />
<input {...api().getInputProps()}/>
<button {...api().getIncrementTriggerProps()} ></button>
</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 [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: "number-input",
}),
)

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,
},
}),
)

Disabling long press spin

To disable the long press spin, set the spinOnPress to false.

const [state, send] = useMachine(
numberInput.machine({
spinOnPress: false,
}),
)

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 QR Code 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 readonly
invalid
boolean
Whether the number input value is invalid.
required
boolean
Whether the number input is required
pattern
string
The pattern used to check the <input> element's value against
value
string
The value of the input
min
number
The minimum value of the number input
max
number
The maximum value of the number input
step
number
The amount to increment or decrement the value by
allowMouseWheel
boolean
Whether to allow mouse wheel to change the value
allowOverflow
boolean
Whether to allow the value overflow the min/max range
clampValueOnBlur
boolean
Whether to clamp the value when the input loses focus (blur)
focusInputOnChange
boolean
Whether to focus input when the value changes
translations
IntlTranslations
Specifies the localized strings that identifies the accessibility elements and their states
formatOptions
Intl.NumberFormatOptions
The options to pass to the Intl.NumberFormat constructor
inputMode
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 devices
onValueChange
(details: ValueChangeDetails) => void
Function invoked when the value changes
onValueInvalid
(details: ValueInvalidDetails) => void
Function invoked when the value overflows or underflows the min/max range
onFocusChange
(details: FocusChangeDetails) => void
Function invoked when the number input is focused
spinOnPress
boolean
Whether to spin the value when the increment/decrement button is pressed
locale
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.

Machine API

The Number Input api exposes the following methods:

focused
boolean
Whether the input is focused.
invalid
boolean
Whether the input is invalid.
empty
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.

Data Attributes

Root

name
desc
data-scope
number-input
data-part
root
data-disabled
Present when disabled
data-focus
Present when focused
data-invalid
Present when invalid

Label

name
desc
data-scope
number-input
data-part
label
data-disabled
Present when disabled
data-focus
Present when focused
data-invalid
Present when invalid

Control

name
desc
data-scope
number-input
data-part
control
data-focus
Present when focused
data-disabled
Present when disabled
data-invalid
Present when invalid

Value Text

name
desc
data-scope
number-input
data-part
value-text
data-disabled
Present when disabled
data-invalid
Present when invalid
data-focus
Present when focused

Input

name
desc
data-scope
number-input
data-part
input
data-invalid
Present when invalid
data-disabled
Present when disabled

Decrement Trigger

name
desc
data-scope
number-input
data-part
decrement-trigger
data-disabled
Present when disabled

Increment Trigger

name
desc
data-scope
number-input
data-part
increment-trigger
data-disabled
Present when disabled

Scrubber

name
desc
data-scope
number-input
data-part
scrubber
data-disabled
Present when disabled

Accessibility

Keyboard Interaction

name
desc
Space
Activates/Selects the highlighted item
Enter
Activates/Selects the highlighted item
ArrowDown
Highlights the next item in the menu
ArrowUp
Highlights the previous item in the menu
ArrowRight/ArrowLeft
When focus is on trigger, opens or closes the submenu depending on reading direction.
Esc
Closes the menu and moves focus to the trigger