Number field ported
A bounded number with odometer digits and steppers.
A bounded number with odometer digits and steppers.
Loading demo…
Guidance
When to use
- Any bounded numeric value with clear increment and decrement controls, such as seats, quantity, or retries.
- Values typed often and stepped often, where arrow keys, PageUp/PageDown, and hold-to-repeat speed up entry.
- Prefixed or suffixed amounts where the unit should follow the value.
When not to use
- Use input for free-form numeric strings like serials or codes.
- Use slider when the value is picked from a range by position rather than typed.
- Use several fields with one save button — number-field commits every change as it happens.
pnpm add sveltearcUsage
<script lang="ts">
import { NumberField } from '$lib/components/number-field';
let seats = $state(4);
</script>
<NumberField label="Seats" bind:value={seats} min={1} max={12} suffix={(n) => n === 1 ? ' seat' : ' seats'} description="How many people need access." />Variants & Examples
Default
Standard appearance and configuration.
<NumberField label="Seats" />API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | – | (Required) Visible label, tied to the input. Doubles as the scrub handle when scrub is set. |
value | number | – | Controlled value. Omit (with defaultValue) for uncontrolled use; bindable. |
defaultValue | number | 0 | Initial value for uncontrolled use. |
onValueChange | (value: number) => void | – | Fires with the clamped, snapped value on every committed change. |
min | number | 0 | Lower bound. |
max | number | Number.MAX_SAFE_INTEGER | Upper bound. |
step | number | 1 | Step size. Non-positive values fall back to 1. |
largeStep | number | ten steps | PageUp, PageDown, and Shift with an arrow move this far. |
description | string | – | Helper copy under the field, linked through aria-describedby. |
disabled | boolean | false | Disables the input and both step buttons. |
id | string | $props.id() | Element id. Falls back to an auto-generated id. |
prefix | string | ((value: number) => string) | – | Text before the number, such as "$". |
suffix | string | ((value: number) => string) | – | Text after the number, such as " seats". |
scrub | boolean | false | Drag the label sideways to scrub the value, one step every few pixels. |
locale | string | "en-US" | Formatting locale. Fixed by default so server and client render the same digits. |
formatOptions | { minimumFractionDigits?: number; maximumFractionDigits?: number; useGrouping?: boolean } | – | Fraction digits and grouping. Fraction digits follow the precision of step by default. |
size | "sm" | "md" | "lg" | "md" | Control height, type size, and default width. |
limitHint | boolean | ((edge: "min" | "max", limit: number) => string) | true | A short note beside the label when a press meets a limit. false hides it; a function writes the copy. |
Keyboard Interactions
| Key | Action |
|---|---|
ArrowUp / ArrowDown | Steps the value; hold to repeat. Shift takes a large step. |
PageUp / PageDown | Takes a large step. |
Home / End | Jumps to the minimum or maximum. |
Enter | Commits a typed draft, or selects the value when idle. |
Escape | Rolls a typed draft back to the value it started from. |
Tab | Moves focus to the interactive element. |
Accessibility
- Renders a native text input with role="spinbutton" plus aria-valuenow, aria-valuetext, aria-valuemin, and aria-valuemax.
- Description and limit-note ids are merged into aria-describedby alongside any caller value; a typed value past a limit sets aria-invalid.
- Limit presses announce which limit was met through a polite live region; animated digits are aria-hidden with a plain screen reader value.
Motion
Phase 1 still-port: odometer wheels, affix rolls, limit-note slide, value strain, and helper word-rise render their static end states; Phase 2 wires the springs without restructuring.
Notes for AI
- Bounded numeric entry with steppers. Commits every change live — do not use where several fields must save together.
- Works controlled or with defaultValue; onValueChange always receives the clamped, snapped value.
- Typed drafts apply live while valid; derive limit copy with limitHint instead of custom messages.
Source of truth: registry/components/number-field/number-field.tsx