NumberInputbeta
Numeric input with steppers, a clamped range, and decimal precision. It builds on Input, so its sizes, variants, and error/helper display carry over.
Playground
<NumberInput
helper="Use the steppers or the arrow keys."
label="Quantity"
max={10}
min={0}
precision={0}
/>01 Examples
Quantity in a line item
min keeps it out of negatives, max caps it at what is in stock, and the stepper clamps immediately instead of waiting for blur.<NumberInput label="Quantity" bind:value={quantity} min={0} max={99} step={1} />Decimal rate with fixed precision
step and precision are separate: step is how far one press moves, precision is how many decimals are displayed and rounded to. Without precision the value shows as typed and the step's own decimals drive the rounding.<NumberInput
label="Commission rate"
bind:value={rate}
min={0}
max={1}
step={0.05}
precision={2}
helper="Between 0 and 1"
/>Without the stepper
hideStepper removes the buttons; Arrow keys and the wheel still step. Useful in a dense row where two more hit targets per field would crowd the layout.<NumberInput label="Servings" bind:value={servings} min={1} max={12} hideStepper />02 Customization
NumberInput wraps <Input>, so size, variant, label, helper, error and the Input slotClasses keys all behave exactly as they do there. A
few Input props are held back: the raw string value, the fixed type/inputmode,
the numeric event handlers, and clearable. Input's clear button would overwrite the
numeric value with its own string, so NumberInput leaves it out.
rightIcon puts your own adornment where the stepper
sits. The Arrow keys still step, but the +/− buttons are gone, so pair it with hideStepper or add your own controls. See the global Customization guide for the general contract.
For money use CurrencyInput instead: it stores minor units, so summing and comparison stay exact. NumberInput's values are plain numbers and carry the usual float caveats.
03 Accessibility
Spinbutton semantics
The field is a role="spinbutton" carrying aria-valuenow, aria-valuemin and aria-valuemax, so assistive tech announces both the
current value and the range it sits in.
Keyboard
↑ / ↓ step by step and
clamp immediately. Typing is free-form while focused: a leading - and a single decimal separator (. or ,) are accepted, and the value clamps to the range
on blur.
The steppers stay out of the tab order
Both stepper buttons carry aria-hidden and tabindex="-1". The spinbutton role already exposes increment and decrement
on the field itself, so the buttons are the pointer affordance for what the Arrow keys do, not
a second pair of tab stops.
Numeric keyboard on mobile
inputmode="decimal" is fixed, so mobile keyboards open
the numeric pad with a decimal separator rather than the full alphabetic layout.
The wheel only steps a focused field
Wheel stepping is gated on focus. An unfocused field scrolled past in a long form would otherwise change its value while the user is only trying to reach the bottom of the page.
04 API Reference
35 propsProp | Type | Default | Description | |
|---|---|---|---|---|
autoComplete inherited | string | — | HTML autocomplete hint for browser autofill. | |
class inherited | string | — | Extra classes merged onto the root wrapper element. | |
disabled inherited | boolean | false | disabled property | |
error inherited | string | — | Error message below the input. When set, overrides helper and
forces danger border styling regardless of intent. | |
helper inherited | string | — | Helper text below the input — hidden when error is present. | |
hideStepper | boolean | false | Hide the up/down stepper buttons (Arrow keys + wheel still work). | |
label inherited | string | — | Label text displayed above the input, auto-linked via for/id. | |
leftIcon inherited | Snippet | — | Icon snippet rendered on the left side of the input field. | |
leftIconAriaLabel inherited | string | — | Accessible label for the clickable left icon button. Required when
onLeftIconClick is set so screen-reader users hear a name for the
button (icons inside are aria-hidden). | |
max | number | — | Maximum allowed value. Clamped on step / Arrow / blur. | |
min | number | — | Minimum allowed value. Clamped on step / Arrow / blur. | |
mint | InputProps['mint'] | 'none' | Micro-interaction preset forwarded to the inner Input. Redeclared from InputProps so the inheritance is a documented contract rather than an accident of the Omit list. | |
name | string | — | Shared name for a hidden input for native form submission. | |
onLeftIconClick inherited | () => void | — | When provided, the left icon becomes a clickable button. | |
onRightIconClick inherited | () => void | — | When provided, the right icon becomes a clickable button. | |
onValueChange | (value: number | null) => void | — | Fires after the value changes (typing, stepper, Arrow, wheel, or clamp). | |
persistDebounceMs inherited | number | 300 | Debounce interval (ms) for storage writes. | |
persistKey inherited | string | — | Key for persisting the input value to storage. | |
persistNamespace inherited | string | — | Namespace (e.g. user id) to scope the persist key. | |
persistStorage inherited | localStoragesessionStorage | 'localStorage' | Storage backend for persistence. | |
persistVersion inherited | number | 1 | Version stamp included in the storage key. | |
precision | number | — | Fixed number of decimal places for display and rounding. When unset, the value is shown as typed and the step's own decimals drive rounding. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ NumberInput: {...} }}>.
Resolved against the **NumberInput** key, not Input: a preset written for
the number field would otherwise style every text field under the provider.
defaults.Input still applies — the resolved preset reaches Input as
instance slotClasses, so it wins over the provider's input-wide defaults
and loses to slotClasses / class written on this component.
A preset's overrides rules are matched against what you wrote here plus
Input's own variant defaults; an axis Input derives or coerces for itself
(tier, messageType, error, hasRightIcon, iconPosition) can match
the wrong state — #360. | |
readonly inherited | boolean | false | readonly property | |
required inherited | boolean | false | Adds a required asterisk to the label and sets the native required attribute. | |
rightIcon | Snippet | — | A custom right-side adornment. Overrides the stepper — pair with hideStepper or provide your own controls. | |
rightIconAriaLabel inherited | string | — | Accessible label for the clickable right icon button. Required when
onRightIconClick is set so screen-reader users hear a name for the
button (icons inside are aria-hidden). | |
slotClasses | Partial<Record<NumberInputSlots | keyof NonNullable<InputProps['slotClasses']>, string>> | — | Per-slot class overrides. Carries NumberInput's own two slots — stepper
(the button column) and stepperButton — on top of every InputProps
slot. One record reaches both: the stepper is painted here and the rest
travels on to the inner field. From <BlocksProvider>, everything under
NumberInput reaches both halves; defaults.Input reaches the field only. | |
step | number | 1 | Increment applied by the stepper buttons, Arrow keys, and wheel. | |
unstyled inherited | boolean | — | Remove all default tv() classes — only user-provided classes apply. | |
value | number | null | — | Current numeric value. null when the field is empty. Supports bind:value. |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
NumberInputProps | interface | props | 0 | — | |
NumberInputVariants | type | variant | 0 | — | |
NumberInputSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. | |
InputProps | interface | props | 0 | — | |
InputVariants | type | variant | 0 | — | |
MintProp | type | helper | 2 | — | |
InputSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. | |
VariantProps | type | helper | 0 | — | |
MintName | type | helper | 0 | A mint name: a built-in (autocompleted), 'none' to disable, or any
consumer-registered name. (string & {}) keeps the registry open — a
custom name still type-checks, it just isn't suggested. A typo therefore
also still compiles (it resolves like an unregistered custom name and
warns at runtime); the union buys completion and docs, not validation. | |
MintConfig | interface | helper | 0 | — | |
SlotNames | type | helper | 0 | Extracts the slot-name union from a slotted tv() config function — the
companion to VariantProps. The slot-mode overload returns
(props?) => { [K in keyof S]: SlotFn }, so keyof ReturnType<T> is exactly
the set of slot names a component declares in tv({ slots: … }).
Use it to type a component's slotClasses prop from the single source of
truth (its *.variants.ts) instead of hand-maintaining a parallel union
that silently drifts when a slot is added or renamed: |
06 Installation
Import
import { NumberInput } from '@urbicon-ui/blocks';