Skip to content
omaris

Inputs

Number Input

A number field that is a text field underneath.

import { NumberInput } from 'omaris'
Learn

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, and wrap when the range is a cycle.
  • An optional number. Clearing the field gives null, not 0, so "no limit" and "a limit of zero" stay different.
  • A measurement, with precision and a suffix: 2.5 kg, 30 %.
  • The same bounded number on a phone. picker leaves 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. picker here 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 min when the number cannot go below one. The − stepper then stops, and the first + on an empty field starts at the minimum.
  • Set precision to the decimals the value really has. The field rounds to it on blur and step moves in it.
  • Use layout="stacked" in a dense table row and layout="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 group on for anything over a thousand. It formats only while the field is idle, so the caret never moves.
  • Give picker both a min and a max. Without ends the field stays a field.

Don't

  • Turn wheel on 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 picker on 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 with invalid and a supportingText.
  • Hide the label. aria-label is 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 bindable

Defaults 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.

plain
tonal
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 bindable

Defaults to null

HTMLInputElement | null