Number Field
Accessible spinbutton with locale-aware formatting, keyboard stepping, and hold-to-repeat stepper buttons. Built from scratch — no bits-ui primitive exists for this one.
Usage
number-field.svelte
<script lang="ts">
import { NumberField } from '$lib/components/ui/number-field';
import { Label } from '$lib/components/ui/label';
let value = $state(0);
</script>
<div class="flex flex-col gap-1.5">
<Label for="nf-basic">Quantity</Label>
<NumberField id="nf-basic" bind:value min={0} max={100} />
</div>Min / Max
Values are clamped on blur, Enter, and each stepper click. Home/End jump to the boundary.
number-field.svelte
<NumberField bind:value min={0} max={100} />Custom step
Buttons and arrow keys step by 5. Hold the button for continuous repeat.
number-field.svelte
<NumberField bind:value min={0} max={100} step={5} />Keyboard modifiers
Arrow keys step by 1. Shift+Arrow steps by 0.1 (fine). Page Up/Down steps by 10 (coarse).
number-field.svelte
<!-- ArrowUp/Down = ±1, Shift+Arrow = ±0.1, PageUp/Down = ±10 -->
<NumberField bind:value step={1} smallStep={0.1} largeStep={10} />Formatted — currency
On blur the value is formatted via Intl.NumberFormat. On focus the raw number is shown for unambiguous editing.
number-field.svelte
<NumberField
bind:value
min={0}
format={{ style: 'currency', currency: 'USD' }}
/>Formatted — percentage
number-field.svelte
<NumberField
bind:value
min={0}
max={1}
step={0.01}
smallStep={0.001}
format={{ style: 'percent', minimumFractionDigits: 1 }}
/>Disabled
number-field.svelte
<NumberField value={42} disabled />Prefix / Suffix
String decorators rendered as muted spans outside the input but inside the field.
°F
number-field.svelte
<NumberField bind:value min={0} suffix="°F" />API reference
Scratch-built spinbutton — no bits-ui dependency. All props are native to this component.
| Prop | Type | Default | Description |
|---|---|---|---|
number (bindable) | 0 | The current numeric value. Two-way bindable. | |
number | — | Minimum allowed value. Clamped on commit. Home key jumps here. | |
number | — | Maximum allowed value. Clamped on commit. End key jumps here. | |
number | 1 | Delta applied by stepper buttons and bare arrow keys. | |
number | step | Delta applied by Shift+ArrowUp/Down (fine control). | |
number | step × 10 | Delta applied by Page Up / Page Down (coarse control). | |
Intl.NumberFormatOptions | — | Passed to Intl.NumberFormat on blur. Omit for plain number display. Raw value shown on focus regardless. | |
string | — | BCP 47 locale tag (e.g. "en-US", "de-DE"). Defaults to the browser locale when omitted. | |
string | — | Static string rendered before the input inside the field. | |
string | — | Static string rendered after the input inside the field. | |
boolean | false | Disables all interaction and drops opacity to 50%. | |
boolean | false | Prevents text editing and stepper interaction. Value is still selectable. | |
(value: number) => void | — | Callback fired on blur or Enter after the value is clamped and committed. | |
HTMLInputElement | null (bindable) | null | Two-way-bindable reference to the inner <input> element. | |
string | — | Merged onto the root wrapper via tailwind-merge. |