Number Field
Locale-aware APG spinbutton with formatting, stepping, press-and-hold triggers, and form association.
|
|
| Package |
@vizejs/ui/number-field |
| Maturity |
stable |
| Own the source |
vize lib pull number-field |
| Requires |
context, controllable-state, id, locale |
| Aliases |
number input, spinbutton, stepper input, currency input, numeric field |
| Covers |
HTML number input, WAI-ARIA Spinbutton, React Aria NumberField, Radix/Reka NumberField |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { NumberField, NumberFieldInput, NumberFieldIncrement, NumberFieldDecrement } from "@vizejs/ui/number-field";
Or copy the source into your project with vize lib pull number-field (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Quantity spinbutton bound with v-model, clamped between 1 and 10 with stepper buttons. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import {
NumberField,
NumberFieldDecrement,
NumberFieldIncrement,
NumberFieldInput,
type NumberFieldValue,
} from "@vizejs/ui/number-field";
const quantity = ref<NumberFieldValue>(2);
const inputId = useId();
</script>
<template>
<div>
<label :for="inputId">Tickets</label>
<NumberField :id="inputId" v-model="quantity" name="tickets" :min="1" :max="10" locale="en-US">
<NumberFieldDecrement aria-label="Remove a ticket">−</NumberFieldDecrement>
<NumberFieldInput />
<NumberFieldIncrement aria-label="Add a ticket">+</NumberFieldIncrement>
</NumberField>
<output :for="inputId">{{ quantity ?? 0 }} tickets selected</output>
</div>
</template>
API
NumberField
Source: src/families/form/number-field/number-field.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Id of the spinbutton input. null and undefined select a deterministic fallback. Binding a Field's fieldProps here wires the label, description, and error. |
name |
string |
undefined |
Native form field name. When set, a hidden input submits the raw number (not the formatted text) with the owning form. |
form |
string |
undefined |
Id of a form owner outside the component tree. |
modelValue |
NumberFieldValue |
undefined |
Controlled value. undefined selects uncontrolled behavior; null is empty. |
defaultValue |
NumberFieldValue |
null |
Initial uncontrolled value and the value restored by form reset. |
min |
number |
undefined |
Lower bound. Omit for an unbounded field. |
max |
number |
undefined |
Upper bound. Omit for an unbounded field. |
step |
number |
1 (0.01 when formatOptions.style is "percent") |
Positive step used by arrow keys, triggers, and the wheel. |
largeStep |
number |
step * 10 |
Positive step used by Page Up and Page Down. |
locale |
string |
undefined |
BCP 47 locale for formatting and parsing. Falls back to the nearest LocaleProvider, then the document language, then en-US. |
formatOptions |
NumberFieldFormatOptions |
undefined |
Intl.NumberFormat options, for example currency, percent, or unit styles. |
clampOnCommit |
boolean |
true |
Clamp typed values into [min, max] when they are committed. |
snapOnCommit |
boolean |
false |
Snap typed values to the nearest step when they are committed. |
allowWheel |
boolean |
false |
Opt in to stepping with the mouse wheel while the input is focused. |
holdDelay |
number |
400 |
Delay before press-and-hold on a trigger starts repeating, in milliseconds. |
holdInterval |
number |
60 |
Interval between repeated steps while a trigger is held, in milliseconds. |
disabled |
boolean |
false |
Disable editing, focus, triggers, and native form submission. |
readOnly |
boolean |
false |
Keep the input focusable while preventing user changes. |
required |
boolean |
false |
Mark the input as required for native constraint validation. |
ariaLabel |
string |
undefined |
Accessible name when no label or aria-labelledby supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids that label the input. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the input. |
ariaErrormessage |
string |
undefined |
Id of the validation error message used while invalid. |
ariaInvalid |
NumberFieldAriaInvalid |
false |
Invalid state announced to assistive technology. |
Events
| Event |
Payload |
Description |
update:modelValue |
[value: NumberFieldValue] |
Fired when a commit or step requests a new controlled value. |
change |
[value: NumberFieldValue, previous: NumberFieldValue, source: NumberFieldChangeSource] |
Fired after a distinct value is committed, with the previous value and its source. |
Slots
| Slot |
Slot props |
Description |
default |
NumberFieldSlotState |
Renders the input, triggers, and any adornments with normalized state. |
Exposed
| Member |
Type |
Description |
value |
NumberFieldValue |
Committed numeric value, or null while empty. |
formattedValue |
string |
Locale-formatted committed value, or "" while empty. |
inputText |
string |
Text currently shown in the input, including uncommitted typing. |
min |
number |
Normalized lower bound (-Infinity when unbounded). |
max |
number |
Normalized upper bound (Infinity when unbounded). |
step |
number |
Normalized positive step. |
largeStep |
number |
Normalized positive large step. |
locale |
string |
Resolved locale used for formatting and parsing. |
canIncrement |
boolean |
Whether an increment can still change the value. |
canDecrement |
boolean |
Whether a decrement can still change the value. |
disabled |
boolean |
Whether editing and triggers are disabled. |
readOnly |
boolean |
Whether user edits are locked while focus remains available. |
required |
boolean |
Whether native required validation is requested. |
invalid |
boolean |
Whether assistive technology announces the field as invalid. |
state |
NumberFieldState |
Stable state token for styling and tests. |
root |
HTMLDivElement | null |
Rendered root element. |
input |
HTMLInputElement | null |
Registered spinbutton input, once mounted. |
focus |
(options?: FocusOptions) => void |
Focus the spinbutton input. |
setValue |
(value: NumberFieldValue) => boolean |
Request a value (clamped, and snapped when snapOnCommit) and report whether it changed. |
increment |
(count?: number) => boolean |
Step up by count steps (default 1) and report whether the value changed. |
decrement |
(count?: number) => boolean |
Step down by count steps (default 1) and report whether the value changed. |
commit |
() => boolean |
Parse and commit the current input text; returns whether the value changed. |
reset |
() => boolean |
Restore the default value and report whether it changed. |
Source: src/families/form/number-field/number-field-input.vue
Props
| Prop |
Type |
Default |
Description |
placeholder |
string |
undefined |
Native placeholder text shown while empty. |
autocomplete |
string |
"off" |
Native autocomplete hint. |
Events
| Event |
Payload |
Description |
reject |
[text: string, nativeEvent: Event] |
Fired when typed text is rejected because it cannot become a number. |
NumberFieldIncrement
Source: src/families/form/number-field/number-field-increment.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
"Increase" or "Decrease" |
Accessible name of the trigger. |
Slots
| Slot |
Slot props |
Description |
default |
NumberFieldTriggerSlotState |
Trigger contents, typically an icon. |
NumberFieldDecrement
Source: src/families/form/number-field/number-field-decrement.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
"Increase" or "Decrease" |
Accessible name of the trigger. |
Slots
| Slot |
Slot props |
Description |
default |
NumberFieldTriggerSlotState |
Trigger contents, typically an icon. |
Behavior
Normative state x input -> outcome table for number-field.vue,
number-field-input.vue, number-field-increment.vue, and
number-field-decrement.vue (@vizejs/ui/number-field). The input follows the
WAI-ARIA APG spinbutton pattern. Every row is proven by the named test in
number-field.test.ts, number-field-parser.test.ts, or
number-field-ssr.test.ts; compile-only assertions live in
number-field.types.test-d.ts.
| # |
State |
Input |
Outcome |
Proven by |
| N1 |
named, bounded |
render |
role="spinbutton" text input with deterministic id, locale text, aria-valuenow/min/max/valuetext, inputmode, required state, hidden raw-number form value, parts |
renders an APG spinbutton with locale text, bounds, and form hooks |
| N2 |
editable |
type, then blur |
partial text (-, 1,234.) is kept without emitting; blur parses, commits, re-formats, emits update:modelValue then change(value, previous, "blur"); empty -> null |
typing keeps partial text and commits the parsed value on blur |
| N3 |
editable |
type an impossible character |
the previous text is restored and reject(text, event) fires; a minus sign is rejected while min >= 0 |
rejects characters that can never form a number |
| N4 |
bounded |
ArrowUp/Down, PageUp/Down, Home/End |
arrows move to the adjacent step-grid value, pages move by largeStep (default step * 10), Home/End jump to finite bounds, all clamped; keys are prevented |
arrow, page, home, and end keys step along the grid within bounds |
| N5 |
unbounded |
Home / End |
native caret movement is kept (not prevented); no aria-valuemin/max; signed fields use a text keyboard |
unbounded fields keep native Home and End caret movement |
| N6 |
uncommitted typing |
arrow key |
stepping starts from the typed text; decimal steps never accumulate floating-point error |
typed text is the stepping origin and precision stays decimal-safe |
| N7 |
commit |
blur / Enter |
typed values clamp into bounds unless clampOnCommit=false; snapOnCommit snaps to the step grid; Enter commits with source "enter" |
commits clamp by default and snap when requested |
| N8 |
locale + formatOptions |
render / type |
currency, percent (1% default step), and unit styles format and parse per locale; zero-fraction currencies reject decimals and use a numeric keyboard |
formats and parses currency, percent, and unit styles per locale |
| N9 |
triggers |
click / press-and-hold |
AT click steps once; pointer press keeps focus in the input, steps, then repeats after holdDelay every holdInterval until release/leave/cancel or a bound disables it |
triggers step on click, disable at bounds, and repeat while held |
| N10 |
wheel |
wheel |
ignored unless allowWheel; when opted in, only a focused input steps and cancels page scrolling (non-passive listener) |
wheel stepping is opt-in and requires focus |
| N11 |
controlled |
step request |
emits the request; the input keeps showing modelValue until the parent accepts it |
controlled value wins until the parent accepts the request |
| N12 |
in a form |
submit / reset |
the hidden input submits the raw number (not formatted text); form reset restores defaultValue and its formatted text |
submits the raw number and restores the default on form reset |
| N13 |
disabled / read-only |
keys, typing, triggers |
disabled: native disabled input, triggers, and hidden value; read-only: focusable with aria-readonly, triggers disabled, no edits or emits |
disabled and read-only fields keep availability semantics |
| N14 |
imperative |
expose |
focus, setValue (clamped, NaN -> null), increment(count), decrement(count), commit, reset, and normalized state |
exposes focus, setValue, increment, decrement, commit, and reset |
| N15 |
inside Field |
v-bind="fieldProps" on root |
the spinbutton receives the Field id, aria-labelledby, aria-describedby, aria-errormessage, and aria-invalid |
binds Field fieldProps to wire the label, description, and error |
| N16 |
part without provider |
setup |
throws the stable VIZE_UI_CONTEXT_MISSING: NumberField diagnostic |
parts require a NumberField provider |
| N17 |
parser |
format -> parse |
round-trips across locales, native numbering systems, accounting currency, percent, and unit styles; partial prefixes are accepted while typing |
round-trips formatted text across locales, numbering systems, and styles |
| N18 |
SSR / hydration |
isolated requests |
byte-identical markup, no NaN/Infinity, and hydration without diagnostics; hydrated triggers remain interactive |
renders byte-identical spinbutton markup and hydrates without mismatches |
| N19 |
inside LocaleProvider |
render |
without a locale prop the field formats and parses with the provider locale |
inherits the locale from the nearest LocaleProvider |
Public extension contract
| Surface |
Contract |
| Parts |
root (<div>), input (spinbutton), increment, decrement (<button tabindex="-1">). |
| Data attributes |
data-vize-ui (number-field, number-field-input, number-field-increment, number-field-decrement, number-field-value), data-state on root/input. |
| Boolean hooks |
Root: data-disabled, data-readonly, data-required, data-invalid, data-empty. Triggers: data-disabled, data-holding. |
| Slots |
Root default slot receives NumberFieldSlotState; trigger slots receive { direction, disabled, holding }. |
| Field wiring |
Bind a Field's fieldProps to NumberField; the root forwards the id and ARIA relations to the spinbutton and triggers reference it via aria-controls. |
| Locale |
locale prop, else the nearest LocaleProvider, else the document language, else en-US. |
The subpath is tree-shakable and ships no CSS; those package contracts are
pinned by distribution.test.ts, check:size, and check:tree-shaking.