Skip to content

Number Input

Form InputsSince 0.2.0
import { NumberInput } from '@absoluteui/components/number-input';
import '@absoluteui/components/number-input/style.css';

Using Angular? See Number Input for Angular.

Locale-aware numeric entry. NumberInput renders a text <input> with the spinbutton role (not native type=number, which ignores the locale’s separators and differs across browsers). It parses and formats group and decimal separators, locale digits (for example Arabic-Indic), minus-sign variants, and currency, percent, and unit literals through Intl.NumberFormat. It steps with the keyboard and optional stepper buttons, validates range and step with localized messages, and submits a canonical value through a hidden input.

  • Quantities, amounts, prices, percentages, and measurements users type or adjust in small increments.
  • Numbers that must be entered and displayed in the user’s locale (1.234,5 in German, Arabic-Indic digits in Arabic).
  • Values constrained by a minimum, maximum, or step.
  • Identifiers made of digits (card numbers, postal codes, phone numbers, one-time codes): use Text Input with inputMode ‘numeric’.
  • Picking a value on a continuous range where precision does not matter: use Slider.
  • Dates and times: use Date Input or Date Picker.
import { NumberInput } from '@absoluteui/components/number-input';
const host = document.createElement('div');
document.body.append(host);
const quantity = NumberInput.create(host, { label: 'Quantity', name: 'quantity', value: 1, min: 1, max: 99, required: true });
host.addEventListener('aui-change', () => {
host.dataset.quantity = String(quantity.value ?? '');
});
import { NumberInput } from '@absoluteui/components/number-input';
const host = document.createElement('div');
document.body.append(host);
NumberInput.create(host, {
label: 'Preis',
name: 'price',
locale: 'de-DE',
min: 0,
step: 0.01,
format: { style: 'currency', currency: 'EUR', maximumFractionDigits: 2 },
});
import { NumberInput } from '@absoluteui/components/number-input';
const host = document.createElement('div');
document.body.append(host);
NumberInput.create(host, {
label: 'Discount',
min: 0,
max: 1,
step: 0.001,
format: { style: 'percent', maximumFractionDigits: 1 },
validate: (value) => (value !== null && value > 0.5 ? 'Discounts above 50% need approval.' : null),
});
import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { numberInput: { showSteppers: false, validationTiming: 'blur' } } });
import { NumberInput } from '@absoluteui/components/number-input';
import '@absoluteui/components/number-input/style.css';
const host = document.querySelector<HTMLElement>('#quantity');
if (host) {
const quantity = NumberInput.create(host, { label: 'Quantity', name: 'quantity', value: 1, min: 1, max: 99, required: true });
host.addEventListener('aui-change', () => {
if (!quantity.validity.valid) quantity.reportValidity();
});
}
import { NumberInput } from '@absoluteui/components/number-input';
const host = document.createElement('div');
document.body.append(host);
NumberInput.create(host, { label: 'Preis', name: 'price', locale: 'de-DE', min: 0, step: 0.01, format: { style: 'currency', currency: 'EUR', maximumFractionDigits: 2 } });
<div class="aui-field aui-number-input" data-aui-control> (consumer host)
<label class="aui-field__label" for=input>Label<span class="aui-field__required" aria-hidden="true">*</span></label>
<div class="aui-number-input__control">
<input class="aui-number-input__input" type="text" role="spinbutton" inputmode="decimal"
aria-valuenow aria-valuetext aria-valuemin aria-valuemax>
<button class="aui-number-input__stepper aui-number-input__decrement" tabindex="-1"> (showSteppers)
<button class="aui-number-input__stepper aui-number-input__increment" tabindex="-1"> (showSteppers)
</div>
<input type="hidden" name value="1234.5"> (canonical value)
<div class="aui-field__description"> (optional)
<div class="aui-field__error aui-validation-message"> (when shown)
</div>
NumberInput.create(host: HTMLElement, options: NumberInputOptions): NumberInput

Global configuration key: numberInput (see Configuration).

Option Type Default Description
label string Visible label and accessible name. Required.
hideLabel boolean false Visually hide the label; it remains the accessible name.
value number | null null Initial value; also the value restored by reset().
name string Form field name, set on the hidden input that carries the canonical value.
placeholder string Supplementary hint; never replaces the label.
description string Help text referenced by aria-describedby.
required boolean false Empty value is invalid; shows the required indicator.
disabled boolean false Disables the input and steppers; the value is not submitted.
readOnly boolean false Value is focusable and submitted but not editable or steppable.
min number Minimum value; also the step base and the Home target.
max number Maximum value; End goes to the largest on-grid value <= max.
step number | null 1 Increment and step-validation grid (base min ?? 0). Default 0.01 for percent style. null: any value, increments of 1.
format { minimumFractionDigits?: number; maximumFractionDigits?: number; useGrouping?: boolean; style?: 'decimal' | 'percent' | 'currency' | 'unit'; currency?: string; unit?: string } Display format (Intl.NumberFormat subset). maximumFractionDigits also rounds the value on commit.
validate (value: number | null) => string | null Consumer validation; return a message to mark the value invalid.
icons Partial<{ error: IconRef; increment: IconRef; decrement: IconRef }> Icon slot overrides.
validationTiming 'blur' | 'input' | 'submit' "blur" When validation messages first appear; afterwards they update live. Globally configurable.
showSteppers boolean true Render increase/decrease buttons (pointer affordance; keyboard users use the arrow keys). Globally configurable.
locale string "en-US" Parsing, formatting, and message locale. Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement Consumer host; its children are owned by the component.
input HTMLInputElement The visible text input (role=spinbutton).
formInput HTMLInputElement Hidden input carrying the canonical value and name.
value number | null Current parsed value; null when empty or unparseable.
text string Text in the input (raw while typing, formatted after commit).
name string | null Form field name.
label string Label text.
disabled boolean Disabled state.
validity FieldValidity { valid, error, message }; computed on every change.
dirty boolean Value differs from the initial value, or the text is unparseable.
touched boolean Focus has left the control at least once.
options Readonly<NumberInputOptions> Current instance options.
Method Description
setValue(value: number | null): void Programmatic value; reformats the display (no events); clears an external custom error.
update(options: Partial<NumberInputOptions>): void Merge and apply options.
reportValidity(): boolean Show messages now and return validity.
setCustomError(message: string | null): void External error (server, cross-field rule); cleared when the user edits.
getFormValue(): number | null Value for form serialization.
focus(): void Focus the input.
reset(): void Restore the initial value and clear dirty/touched/submitted state.
destroy(): void Remove rendering, listeners, registration, and subscriptions.
Event Detail Bubbles Cancelable Description
aui-input { value: number | null; text: string; validity: FieldValidity } Yes No User typed, or stepped (keyboard or stepper buttons, since 0.9.0); value is the parse of the raw text.
aui-change { value: number | null; validity: FieldValidity } Yes No User committed a changed value (blur, Enter, arrow/page/Home/End keys, or a stepper button).

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-field aui-number-input Host.
aui-number-input--invalid | --disabled State modifiers on the host.
aui-field__label | __required | __description | __error Shared field chrome.
aui-number-input__control Control boundary (input and stepper buttons).
aui-number-input__input Visible text input.
aui-number-input__stepper | __increment | __decrement Stepper buttons.
Slot Default icon
error alert-circle
increment plus
decrement minus

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-border-strong | --aui-color-disabled-bg | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-hover | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-md | --aui-control-padding-x | --aui-duration-fast | --aui-easing-standard | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-weight-medium | --aui-icon-size-sm | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-md | --aui-space-1

interface NumberInputFormat {
minimumFractionDigits?: number;
maximumFractionDigits?: number;
useGrouping?: boolean;
style?: 'decimal' | 'percent' | 'currency' | 'unit';
currency?: string;
unit?: string;
}
interface NumberInputInputDetail { value: number | null; text: string; validity: FieldValidity }
interface NumberInputValueDetail { value: number | null; validity: FieldValidity }
interface FieldValidity { readonly valid: boolean; readonly error: ValidationErrorCode | null; readonly message: string }

Pattern: WAI-ARIA APG Spinbutton pattern on a text <input> with a <label for> association.

Key Context Action
Tab / Shift+Tab Any Moves focus to and from the input; stepper buttons are skipped.
ArrowUp / ArrowDown Input focused Increase / decrease by one step (from empty: min, else 0), clamped to [min, max].
PageUp / PageDown Input focused Increase / decrease by ten steps.
Home / End Input focused, min / max set Set the minimum / maximum. Without the bound, native caret movement.
Enter Input focused Commit: reformat the value and fire aui-change; implicit form submission is not prevented.
All editing keys Input focused Native text editing.
Element Role
input spinbutton
stepper buttons button (native), not in the tab order
icons none (aria-hidden=true)
Attribute Element Value
aria-valuenow input Current value; removed when empty or unparseable
aria-valuetext input Value formatted in the locale (for example 15% or $1,234.50)
aria-valuemin / aria-valuemax input min / max when set
aria-describedby input Help text and visible error message ids
aria-invalid input “true” only while an error message is shown
aria-label stepper buttons Localized Increase / Decrease
aria-controls stepper buttons Input id

The control boundary shows the focus indicator (border in color-focus-ring plus a 1px ring). Pressing a stepper button keeps focus in the input.

Criterion Level Note
1.3.1 Info and Relationships A
1.3.5 Identify Input Purpose AA
1.4.3 Contrast (Minimum) AA
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.4.7 Focus Visible AA
2.5.3 Label in Name A
2.5.8 Target Size (Minimum) AA
3.3.1 Error Identification A
3.3.2 Labels or Instructions A
3.3.3 Error Suggestion AA
4.1.2 Name, Role, Value A
4.1.3 Status Messages AA

Automated test results: Accessibility and compliance.

  • The required asterisk is aria-hidden; required state is conveyed by the native required attribute.
  • Errors appear after the first blur by default so users are not interrupted while typing.
  • Validation messages format numbers in the locale and style (for example ‘Enter a value of 1.000 or more.’).
  • The stepper buttons are a pointer affordance with tabindex -1; every action is available from the keyboard on the input.
  • Stepper buttons are disabled at the bounds.
  • Spinbutton announcements (aria-valuetext on step) have not yet been manually verified with NVDA, JAWS, and VoiceOver.
  • Mobile screen readers expose adjustable spinbutton gestures inconsistently for text inputs.

default, focus-visible, empty, filled, disabled, read-only, required, invalid (message shown), bad input, at minimum (decrement disabled), at maximum (increment disabled).

From Trigger To
any user types text kept as typed; value parsed; bad-input when unparseable; dirty
editing blur or Enter committed: value rounded to maximumFractionDigits when set; text reformatted unless unparseable; aui-change if the value changed
any arrow, page, Home/End key, or stepper value stepped, clamped, snapped; text reformatted; aui-change
any focus leaves the host touched; messages shown (blur timing), after the pointer is released if focus left by a click
any reportValidity() submitted; messages shown for every timing
any locale or format changes while not focused text reformatted in the new locale
any reset() initial value, formatted; pristine, untouched
  • The required asterisk is aria-hidden; the native required attribute conveys the state.
  • Errors appear after the first blur by default, then update live.
  • Messages format numbers in the locale and style: Enter a value of 1.000 or more. in German.
  • inputmode="decimal" (or numeric when maximumFractionDigits is 0) selects the virtual keyboard.
  • Stepper buttons are a pointer affordance; pressing one keeps focus in the input. Every action is available from the keyboard.
  • Manual screen-reader verification is pending (see Known limitations).

Full width of its container. The stepper buttons keep their size and the input shrinks. Density tokens change the control height and button width.

Logical properties: the stepper buttons sit at the inline end in both directions. Bidi control marks that Intl inserts in RTL locales are ignored when parsing.

Colors come from role tokens only. In forced-colors mode, focus draws a Highlight outline and disabled steppers use GrayText.

configureAbsoluteUI({ components: { numberInput: { showSteppers: false, validationTiming: 'input' } } });

Changes apply to existing instances immediately. A locale change reformats the text of inputs that are not focused.

Instance options override global and theme defaults. All options are dynamic.

icons.error (default alert-circle), icons.increment (default plus), and icons.decrement (default minus).

All text uses textContent or attributes. Validation messages never include the entered text; only constraint parameters. No style attributes or HTML sinks. CSP-safe.

No persistence, network access, or telemetry. The visible input is not named; a hidden input named name carries the canonical value (1234.5, empty when null or unparseable, and not submitted while disabled).

Four listeners per instance (input, blur, keydown, focusout), two per stepper button, and one configuration subscription. Intl.NumberFormat instances are built once per locale and format and reused. Budget: under 1 ms average per NumberInput.create across 1,000 instances.

Values are JavaScript numbers: exact up to 15 significant digits and integers up to 2^53. 1,000 inputs per page is the performance target.

  • Percent style: typed 15 and 15% are both 0.15; the default step is 0.01.
  • de-DE: 1.234,5 is 1234.5; a lone . is a group separator, so 1.5 is 15.
  • ar-EG: ١٬٢٣٤٫٥ and ASCII 1234.5 are both accepted; the display uses Arabic-Indic digits.
  • -5 (U+2212), -5, and a leading + are accepted.
  • Currency: $1,234.50, 1234.5, and USD 1234.5 parse to the same value.
  • Unparseable text (12abc, two decimal separators) is bad-input: value is null, the text is kept for correction, and the hidden input is empty.
  • Stepping from an off-grid value snaps to the grid in the step direction (8 with step 5 from min 1 goes up to 11, down to 6); the result is rounded to the step’s decimal places (0.2 + 0.1 steps to 0.3).
  • maximumFractionDigits rounds the committed value with decimal (not binary) rounding: 1234.505 becomes 1234.51.
Code Category Cause
NUMBER_INPUT_OPTIONS configuration Missing label; non-finite value, min, or max; min > max; step <= 0; invalid format fields (style, missing currency/unit, fraction digits).
NUMBER_INPUT_FORMAT configuration Intl.NumberFormat rejected the format for the locale (for example an unknown unit).
Validation (validity.error) validation value-missing, bad-input, range-underflow, range-overflow, step-mismatch, custom.

A failed construction leaves no rendering, listeners, or subscriptions behind. A failed update() keeps the previous options.

  • IEEE 754 doubles: no arbitrary-precision decimals or BigInt.
  • Parsing is strict to the locale; group separator positions are not validated.
  • Accounting (parenthesized negatives), scientific, engineering, and compact notation are not parsed.
  • The hidden canonical value uses JavaScript number serialization (exponent notation below 1e-6 and from 1e21).
  • With inputmode="numeric" some mobile keyboards have no minus key.
  • Stepper buttons do not repeat on press-and-hold.
  • Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
  • Native type=number behavior (wheel stepping, browser spin buttons).
  • HTML in the label, help text, or messages.
  • BigInt values.