Inputs
Number Input
A number field that is a text field underneath.
import { NumberInput } from 'omaris' Examples
Basic
A quantity field. Empty clears to null, so nothing sticks at zero.
<script lang="ts"> import { NumberInput } from 'omaris'; let quantity = $state<number | null>(1);</script><div class="w-64 max-w-full"> <NumberInput label="Quantity" bind:value={quantity} min={1} max={99} /></div> Layouts
Where the steppers sit. split is the one to reach for on a phone.
<script lang="ts"> import { NumberInput } from 'omaris'; let trailing = $state<number | null>(1); let split = $state<number | null>(2); let stacked = $state<number | null>(8);</script><div class="flex w-72 max-w-full flex-col gap-4"> <NumberInput label="Trailing" bind:value={trailing} min={0} max={99} /> <NumberInput label="Split" layout="split" emphasis="tonal" bind:value={split} min={1} max={12} /> <NumberInput label="Stacked" layout="stacked" bind:value={stacked} min={0} max={24} /></div> Picker
picker opens a wheel instead of a keypad on a touch screen. 'always' shows it here too.
Weight 72 · Guests 2
<script lang="ts"> import { NumberInput, Text } from 'omaris'; let weight = $state<number | null>(72); let guests = $state<number | null>(2);</script><div class="flex max-w-sm flex-col gap-5"> <NumberInput label="Weight" bind:value={weight} min={30} max={200} suffix="kg" picker="always" layout="split" /> <!-- The same prop as a plain `picker`: a keypad on a desktop, a wheel under a thumb. The steppers work either way. --> <NumberInput label="Guests" bind:value={guests} min={1} max={12} picker /> <Text variant="body-sm" tone="muted"> Weight {weight ?? '—'} · Guests {guests ?? '—'} </Text></div> Units
Decimals, a unit and grouped thousands. The separator appears when the field is left, so it never moves the caret while you type.
<script lang="ts"> import { NumberInput } from 'omaris'; let weight = $state<number | null>(2.5); let budget = $state<number | null>(12500);</script><div class="flex w-72 max-w-full flex-col gap-4"> <NumberInput label="Weight" bind:value={weight} suffix="kg" precision={1} step={0.5} min={0} /> <NumberInput label="Budget" bind:value={budget} prefix="$" step={500} min={0} /></div> Overridden
Every part is reachable: a pill field, round steppers, a monospace value — and the component's own sizing still holds it together.
<script lang="ts"> import { NumberInput } from 'omaris'; let seats = $state<number | null>(2);</script><div class="w-52 max-w-full"> <NumberInput label="Seats" layout="split" emphasis="tonal" bind:value={seats} min={1} max={9} classes={{ field: 'rounded-full', control: 'font-mono', step: 'rounded-full' }} /></div> When to use it
Use it for
- A quantity, a count, a seat or guest number: anything adjusted by one more often than retyped.
layout="split"puts a stepper either side of the value, the easiest shape to hit with a thumb. - A bounded setting like a page size, a retry count or an hour. Use
min,max, andwrapwhen the range is a cycle. - An optional number. Clearing the field gives
null, not0, so "no limit" and "a limit of zero" stay different. - A measurement, with
precisionand asuffix:2.5 kg,30 %. - The same bounded number on a phone.
pickerleaves the desktop field alone and turns the touch one into a tap that opens a wheel, instead of a keypad covering half the screen.
Not for
- Money → Price Input, which knows the currency's decimals, symbol side and step.
- A number chosen by feel, like a volume or a threshold → Slider.
- A number from a short fixed set → Select or Segmented Button.
- Spinning through a long range on a touch screen with no field to fall back to → Picker on its own.
pickerhere is the same wheel behind a field a mouse can still type into. - Free text with digits, like an order number or a postcode → Text Field with
format="numeric".
Do
- Give it a
minwhen the number cannot go below one. The − stepper then stops, and the first + on an empty field starts at the minimum. - Set
precisionto the decimals the value really has. The field rounds to it on blur andstepmoves in it. - Use
layout="stacked"in a dense table row andlayout="none"when the number is always typed, like an amount or a year. Holding a stepper accelerates and multiplies the step after twenty repeats. - Leave
groupon for anything over a thousand. It formats only while the field is idle, so the caret never moves. - Give
pickerboth aminand amax. Without ends the field stays a field.
Don't
- Turn
wheelon inside a scrolling page. A wheel over a focused field would change the value instead of scrolling past, the bug<input type="number">is most disliked for. - Use it as a slider with a hundred steps of one.
- Put
pickeron a range thousands of steps wide. Every row is a real element; past a thousand the field stays a field. - Set
clamp={false}and then show the number as valid. To report an out-of-range value instead of correcting it, pair it withinvalidand asupportingText. - Hide the label.
aria-labelis the fallback, not the plan.
Quick reference
emphasis plain(default)tonal
Filled steppers read as buttons in a dense row; the default is quiet.
API
NumberInput
A number field that is a text field underneath.
<input type="number"> is the wrong control for almost every number a dashboard collects: it cannot be emptied without a fight, it accepts 1e5 and +-, its own spinners are unstyleable, and a scroll over a focused one silently changes the value. This is a plain text input with a format that rejects anything that is not a number, so an empty field means null rather than a 0 that will not delete.
The steppers hold to repeat, accelerate the longer they are held, stop at min/max, and never take focus off the field.
import { NumberInput } from 'omaris' <NumberInput label="Quantity" bind:value min={1} max={99} /><NumberInput layout="split" bind:value suffix="kg" precision={1} /> Parts
Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.
root- Wrapper: label, field, supporting text.
field- The frame: border, background, height, adornment layout.
control- The bare <input>; the frame owns every decoration.
label- No description in the source yet.
adornment- Icon slots at either end of the frame.
affix- Flat text glued to the value — "https://", "kg".
action- An interactive trailing control: reveal, clear.
support- Helper line under the field, and the counter beside it.
counter- No description in the source yet.
step- A stepper button. Sized from the field's own
--field-action. steppers- The pair of steppers, and the gap between them.
Props
value bindableDefaults to null
number | null The number. null when the field is empty — not 0. Bindable.
layout Defaults to 'trailing'
NumberInputLayout Where the steppers sit, or none for a bare field.
emphasis Defaults to 'plain'
NumberInputEmphasis How loud the steppers are.
plaintonal
variant Defaults to 'outline'
InputVariant size Defaults to 'default'
InputSize min number Lower bound. The − stepper stops here, and clamp snaps to it.
max number Upper bound.
step Defaults to 1
number One press of a stepper, or one arrow key. Shift multiplies it by 10.
precision Defaults to 0
number Decimal places. 0 — the default — makes it an integer field.
group Defaults to true
boolean Group the thousands while the field is idle — 12,500.
clamp Defaults to true
boolean Snap into [min, max] when the field is left.
wrap Defaults to false
boolean Wrap past the bounds instead of stopping — 12 → 1 on an hour field. Needs both min and max.
wheel Defaults to false
boolean Change the value on a wheel over the focused field. Off by default: a page that scrolls under the pointer must not edit the form.
picker Defaults to false
boolean | 'always' Collect the number on a wheel instead of a keyboard when the pointer is a finger.
A typed number is fastest with a keyboard in front of you and slowest with a thumb, where the numeric keypad covers half the screen to change a quantity by one. picker leaves the desktop field exactly as it is and turns the touch one into a tap that opens a Picker in a bottom sheet — the steppers and bind:value are unchanged either way.
'always' uses the wheel on a mouse too, for a field whose value is the point of the screen.
It needs min and max — a wheel has to have ends — and a range of at most {@link PICKERMAXROWS} steps, since every row is a real element and a wheel that long is slower than typing. The field stays a plain field when either is missing, rather than opening something unusable.
pickerTitle string Title on the picker sheet. Defaults to the field's own label.
pickerConfirmLabel Defaults to 'Done'
string Confirm button on the picker sheet.
pickerCancelLabel Defaults to 'Cancel'
string Cancel button on the picker sheet.
formatValue (value: number) => string Renders the settled value. Replaces the built-in grouping.
label string supportingText string Helper line under the field. Becomes the error text when invalid.
invalid boolean Inside a Field, defaults to the field's.
disabled boolean Inside a Field, defaults to the field's.
readonly Defaults to false
boolean required boolean Inside a Field, defaults to the field's.
placeholder string prefix string Flat text glued to the start of the value — "$", "#".
suffix string Flat text glued to the end — "kg", "%".
name string Posts the raw number, not the formatted text, under this name.
id string aria-label string aria-describedby string decrementLabel Defaults to 'Decrease'
string Accessible label for the − stepper.
incrementLabel Defaults to 'Increase'
string Accessible label for the + stepper.
haptics Defaults to true
boolean Play a tick on each step once haptics.enabled is on.
supporting Snippet Richer supporting text — a link, a hint.
onchange (value: number | null) => void Fires once per settled value, after clamping.
class string classes NumberInputClasses Per-part Tailwind overrides. class still covers the root.
ref bindableDefaults to null
HTMLInputElement | null