Skip to content
omaris

Inputs

Picker

The frame a set of wheels sits in: the selection band across the middle, the fade at the top and bottom edges, and — when you give it onconfirm — the cancel/confirm row under them.

import { Picker } from 'omaris'
Learn

Examples

Basic

A wheel of dates, in the order this locale writes them. Flick it.

Date of birth
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
<script lang="ts">	import { DatePicker } from 'omaris';​	let date = $state<Date | null>(new Date(2021, 8, 17));</script>​<div class="w-80 max-w-full">	<DatePicker surface="card" title="Date of birth" bind:value={date} monthFormat="short" /></div>

Time

AM/PM on the wheels, always; the value underneath stays "14:30", so it sorts and stores without a second format.

Opens at
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12
1
2
3
4
5
6
7
8
9
10
11
12
00
15
30
45
00
15
30
45
00
15
30
45
00
15
30
45
00
15
30
45
AM
PM

stored as 09:30

<script lang="ts">	import { TimePicker } from 'omaris';​	let opensAt = $state('09:30');</script>​<div class="w-64 max-w-full">	<TimePicker surface="card" title="Opens at" bind:value={opensAt} minuteStep={15} />	<p class="pt-3 text-center text-sm text-muted-foreground">		stored as <code class="font-mono">{opensAt}</code>	</p></div>

Columns

Picker is only the frame — the band, the fade and the actions. Put any wheels you like inside it.

Duration
0 hr
1 hr
2 hr
3 hr
4 hr
5 hr
6 hr
7 hr
8 hr
9 hr
10 hr
11 hr
12 hr
0 hr
1 hr
2 hr
3 hr
4 hr
5 hr
6 hr
7 hr
8 hr
9 hr
10 hr
11 hr
12 hr
0 hr
1 hr
2 hr
3 hr
4 hr
5 hr
6 hr
7 hr
8 hr
9 hr
10 hr
11 hr
12 hr
0 hr
1 hr
2 hr
3 hr
4 hr
5 hr
6 hr
7 hr
8 hr
9 hr
10 hr
11 hr
12 hr
0 hr
1 hr
2 hr
3 hr
4 hr
5 hr
6 hr
7 hr
8 hr
9 hr
10 hr
11 hr
12 hr
00 min
05 min
10 min
15 min
20 min
25 min
30 min
35 min
40 min
45 min
50 min
55 min
00 min
05 min
10 min
15 min
20 min
25 min
30 min
35 min
40 min
45 min
50 min
55 min
00 min
05 min
10 min
15 min
20 min
25 min
30 min
35 min
40 min
45 min
50 min
55 min
00 min
05 min
10 min
15 min
20 min
25 min
30 min
35 min
40 min
45 min
50 min
55 min
00 min
05 min
10 min
15 min
20 min
25 min
30 min
35 min
40 min
45 min
50 min
55 min
<script lang="ts">	import { Picker, PickerColumn, numberOptions } from 'omaris';​	let hours = $state(1);	let minutes = $state(30);</script>​<div class="w-72 max-w-full">	<Picker surface="card" title="Duration">		<PickerColumn label="Hours" options={numberOptions(0, 12)} bind:value={hours} unit="hr" loop />		<PickerColumn			label="Minutes"			options={numberOptions(0, 55, { step: 5, pad: true })}			bind:value={minutes}			unit="min"			loop		/>	</Picker></div>

Sheet

Where a picker belongs on a phone: in a sheet, with the confirm row the onconfirm and oncancel handlers draw for you.

<script lang="ts">	import { Button, DateTimePicker, Sheet, latn } from 'omaris';​	let open = $state(false);	let starts = $state<Date | null>(new Date());	let draft = $state<Date | null>(new Date());​	const format = new Intl.DateTimeFormat(		undefined,		latn({ dateStyle: 'medium', timeStyle: 'short' })	);</script>​<div class="flex flex-col items-center gap-3">	<Button		variant="outlined"		onclick={() => {			draft = starts;			open = true;		}}	>		{starts ? format.format(starts) : 'Pick a time'}	</Button>​	<Sheet bind:open side="bottom" title="Starts">		<DateTimePicker			bind:value={draft}			minuteStep={15}			onconfirm={() => {				starts = draft;				open = false;			}}			oncancel={() => (open = false)}		/>	</Sheet></div>

Overridden

Every part is reachable: a full-bleed band in the accent container, a larger face, seven rows deep instead of five.

Weight
40 kg
41 kg
42 kg
43 kg
44 kg
45 kg
46 kg
47 kg
48 kg
49 kg
50 kg
51 kg
52 kg
53 kg
54 kg
55 kg
56 kg
57 kg
58 kg
59 kg
60 kg
61 kg
62 kg
63 kg
64 kg
65 kg
66 kg
67 kg
68 kg
69 kg
70 kg
71 kg
72 kg
73 kg
74 kg
75 kg
76 kg
77 kg
78 kg
79 kg
80 kg
81 kg
82 kg
83 kg
84 kg
85 kg
86 kg
87 kg
88 kg
89 kg
90 kg
91 kg
92 kg
93 kg
94 kg
95 kg
96 kg
97 kg
98 kg
99 kg
100 kg
101 kg
102 kg
103 kg
104 kg
105 kg
106 kg
107 kg
108 kg
109 kg
110 kg
111 kg
112 kg
113 kg
114 kg
115 kg
116 kg
117 kg
118 kg
119 kg
120 kg
121 kg
122 kg
123 kg
124 kg
125 kg
126 kg
127 kg
128 kg
129 kg
130 kg
131 kg
132 kg
133 kg
134 kg
135 kg
136 kg
137 kg
138 kg
139 kg
140 kg
<script lang="ts">	import { NumberPicker } from 'omaris';​	let weight = $state(72);</script>​<div class="w-44 max-w-full">	<NumberPicker		surface="card"		title="Weight"		bind:value={weight}		min={40}		max={140}		unit="kg"		visible={7}		classes={{ band: 'inset-x-0 rounded-none bg-primary-container' }}		columnClasses={{ face: 'text-xl font-semibold' }}	/></div>

When to use it

Use it for

  • Touch first. A wheel covers a hundred values in one flick. Use it on a phone, a tablet or a kiosk; on a desktop a typed field or a Select is faster.
  • A date far from today, like a birthday or an expiry, with DatePicker. Three wheels beat thirteen taps through a calendar.
  • A time, with TimePicker. The wheels are always 1–12 and AM/PM — omaris never shows a 24-hour clock, and there is no prop to make it. The value stays "14:30" underneath, so it sorts and stores as is.
  • A moment, with DateTimePicker: one wheel of whole days ("Today", "Wed 17 Sep") beside the hour and minute.
  • A number out of a long run, like a weight or an age, with NumberPicker. Anything else with your own wheels: Picker is the frame (band, fade, confirm row) and PickerColumn is one wheel in it.

Not for

  • A date near today, or one picked by looking at the week → a Date Field or Calendar. A wheel hides the shape of the month.
  • A number typed more often than spun, like a quantity or a price → Number Input or Price Input.
  • Fewer than about eight choices → Select, Radio or Segmented Button.
  • A value that needs searching → Combobox.
  • A desktop-only form where every field is typed. One wheel in a column of text fields costs more than it saves.

Do

  • Put it in a Sheet on a phone and a Dialog on a desktop. Pass onconfirm and oncancel so the picker draws its own confirm row.
  • Edit a copy of the value and commit it on confirm, so Cancel has something to undo.
  • Give every wheel a label. It is the accessible name.
  • Use loop for a cycle (hours, minutes, months), never for a bounded run like a year. Keep looping wheels short; the list is repeated to fake the ends.
  • Set minuteStep to what the value really has: 15 for opening hours, 5 for a booking, 1 only for an alarm. Use monthFormat="short" (or "numeric") when three wheels share a phone's width.

Don't

  • Show the stored "14:30" to a person. It is data; read it back through Intl.DateTimeFormat with latn(), which pins the 12-hour clock.
  • Stack more than four wheels. Each one gets too narrow to read or hit.
  • Use it for a value with no natural order, like a country or a category.
  • Put one inside a scrolling column without room around it. A wheel takes the vertical drag that would otherwise scroll the page.
  • Reimplement the momentum. The scrolling is the browser's own.

Quick reference

size Picker
  • sm
  • md (default)
  • lg
surface Picker
  • plain (default)
  • card
  • filled

How the frame itself is drawn.

size PickerColumn
  • sm
  • md (default)
  • lg

API

Picker

The frame a set of wheels sits in: the selection band across the middle, the fade at the top and bottom edges, and — when you give it onconfirm — the cancel/confirm row under them.

It is a surface, not an overlay. Put it in a Sheet on a phone or a Dialog on a desktop; drop it straight into a form when the value is the point of the screen.

Built for a thumb first. A wheel is the fastest way through a hundred values on a touch screen and the slowest on a desktop, where a Select or a typed field wins.

import { Picker } from 'omaris'
<Picker title="Duration">  <PickerColumn label="Hours" options={hours} bind:value={h} unit="h" />  <PickerColumn label="Minutes" options={minutes} bind:value={m} unit="m" /></Picker>

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
No description in the source yet.
header
The title line above the wheels.
stage
Everything the band is drawn behind: the wheels and both fades.
band
The strip marking the centre row.
wheels
The row of wheels. Masked so rows dissolve towards the edges.
actions
The cancel/confirm row. Only rendered when onconfirm is given.
action
One of those buttons.

Props

size

Defaults to 'md'

PickerSize

Row height and type scale.

sm
md
lg
surface

Defaults to 'plain'

PickerSurface

How the frame is drawn.

plain
No frame — the wheels sit on whatever is behind them.
card
A card: a border, the popover background and a soft shadow.
filled
A tinted well, for a picker inside a form.
visible
number

Rows on screen at once. Rounded up to the next odd number.

title
string

A title above the wheels.

disabled

Defaults to false

boolean

Disables every wheel inside.

haptics

Defaults to true

boolean

Play a tick as each row passes the centre, once haptics are on.

onconfirm
() => void

Renders the action row. Called with the value already committed.

oncancel
() => void

Renders a cancel button beside it.

confirmLabel

Defaults to 'OK'

string
cancelLabel

Defaults to 'Cancel'

string
footer
Snippet

Replaces the built-in action row.

children
Snippet

The wheels — one PickerColumn each.

class
string
classes
PickerClasses

Per-part Tailwind overrides. class still covers the root.

DatePicker

Three wheels — day, month, year — in the order this locale writes them.

The day wheel is rebuilt from the month and the year, so February never offers a 30th and a leap year does offer the 29th. min and max grey out the rows outside the range rather than removing them, so the wheel does not change length under the finger.

import { DatePicker } from 'omaris'
<DatePicker bind:value={birthday} max={new Date()} monthFormat="short" />

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
No description in the source yet.
header
The title line above the wheels.
stage
Everything the band is drawn behind: the wheels and both fades.
band
The strip marking the centre row.
wheels
The row of wheels. Masked so rows dissolve towards the edges.
actions
The cancel/confirm row. Only rendered when onconfirm is given.
action
One of those buttons.

Props

value bindable

Defaults to null

Date | null

The date. Bindable. null until a wheel is touched.

min
Date

Earliest selectable date. Also the first year on the wheel.

max
Date

Latest selectable date. Also the last year on the wheel.

locale
string

BCP-47 tag for the month names and the column order.

monthFormat

Defaults to 'long'

'long' | 'short' | 'numeric'

How the months read. numeric for a wheel with no room for names.

weekday

Defaults to false

boolean

Prefix the day with its weekday — "Wed 17".

labels
{ day?: string; month?: string; year?: string }

Accessible names for the three wheels.

onchange
(value: Date) => void

Fires once a wheel settles.

columnClasses
PickerColumnClasses

Per-part overrides for the wheels themselves.

classes
PickerClasses
size
PickerSize

Row height and type scale.

surface
PickerSurface

How the frame is drawn.

visible
number

Rows on screen at once. Rounded up to the next odd number.

title
string

A title above the wheels.

disabled
boolean

Disables every wheel inside.

haptics
boolean

Play a tick as each row passes the centre, once haptics are on.

onconfirm
() => void

Renders the action row. Called with the value already committed.

oncancel
() => void

Renders a cancel button beside it.

confirmLabel
string
cancelLabel
string
footer
Snippet

Replaces the built-in action row.

class
string

DateTimePicker

A day, an hour and a minute in one row of wheels.

The day is one wheel of whole dates — "Today", "Tomorrow", "Wed 17 Sep" — rather than three of day, month and year. It is the shape a datetime actually gets picked in: a booking is next Tuesday at half four, not the 17th and September and 2026.

The range is a year forward from today unless you say otherwise, so a picker for something in the past needs a min.

import { DateTimePicker } from 'omaris'
<DateTimePicker bind:value={startsAt} minuteStep={15} />

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
No description in the source yet.
header
The title line above the wheels.
stage
Everything the band is drawn behind: the wheels and both fades.
band
The strip marking the centre row.
wheels
The row of wheels. Masked so rows dissolve towards the edges.
actions
The cancel/confirm row. Only rendered when onconfirm is given.
action
One of those buttons.

Props

value bindable

Defaults to null

Date | null

The moment. Bindable.

min
Date

First selectable day. Defaults to today.

max
Date

Last selectable day. Defaults to a year from today.

minuteStep

Defaults to 5

number

Minutes between rows.

locale
string

BCP-47 tag for the day labels.

relative

Defaults to true

boolean

Say "Today" and "Tomorrow" instead of their dates.

labels
{ day?: string; hour?: string; minute?: string; meridiem?: string; am?: string; pm?: string; }
onchange
(value: Date) => void
columnClasses
PickerColumnClasses

Per-part overrides for the wheels themselves.

classes
PickerClasses
size
PickerSize

Row height and type scale.

surface
PickerSurface

How the frame is drawn.

visible
number

Rows on screen at once. Rounded up to the next odd number.

title
string

A title above the wheels.

disabled
boolean

Disables every wheel inside.

haptics
boolean

Play a tick as each row passes the centre, once haptics are on.

onconfirm
() => void

Renders the action row. Called with the value already committed.

oncancel
() => void

Renders a cancel button beside it.

confirmLabel
string
cancelLabel
string
footer
Snippet

Replaces the built-in action row.

class
string

NumberPicker

One wheel over a run of numbers.

A hundred values in one flick, which is what a wheel is for. On a desktop, where a typed number is faster, Number Input is the better control.

import { NumberPicker } from 'omaris'
<NumberPicker bind:value={weight} min={30} max={200} unit="kg" />

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
No description in the source yet.
header
The title line above the wheels.
stage
Everything the band is drawn behind: the wheels and both fades.
band
The strip marking the centre row.
wheels
The row of wheels. Masked so rows dissolve towards the edges.
actions
The cancel/confirm row. Only rendered when onconfirm is given.
action
One of those buttons.

Props

value bindable

Defaults to 0

number

The number. Bindable.

min

Defaults to 0

number
max

Defaults to 100

number
step

Defaults to 1

number

The gap between rows.

precision

Defaults to 0

number

Decimal places on each row's label.

unit
string

Drawn small beside every row — "kg", "%".

loop

Defaults to false

boolean

Spin past the ends and come back round.

label

Defaults to 'Value'

string

Accessible name for the wheel.

format
(value: number) => string

Replaces the row's text.

onchange
(value: number) => void
columnClasses
PickerColumnClasses

Per-part overrides for the wheel itself.

classes
PickerClasses
size
PickerSize

Row height and type scale.

surface
PickerSurface

How the frame is drawn.

visible
number

Rows on screen at once. Rounded up to the next odd number.

title
string

A title above the wheels.

disabled
boolean

Disables every wheel inside.

haptics
boolean

Play a tick as each row passes the centre, once haptics are on.

onconfirm
() => void

Renders the action row. Called with the value already committed.

oncancel
() => void

Renders a cancel button beside it.

confirmLabel
string
cancelLabel
string
footer
Snippet

Replaces the built-in action row.

class
string

PickerColumn

One wheel.

The scrolling is the browser's own — overflow-y: scroll with scroll-snap-type: y mandatory — because nothing written in JavaScript matches the momentum a phone gives a native scroller for free. What is scripted is only the part CSS cannot do: the cylinder, where each row is tilted and faded by its distance from the centre, and settling on the row that ends up there.

import { PickerColumn } from 'omaris'
<PickerColumn label="Hour" options={hours} bind:value={hour} loop />

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
The scroller. Holds the snap points and the perspective.
item
One snap target. Never transformed — a scaled row moves its own snap point.
face
The row's face. This is what tilts, fades and scales.
unit
The unit glued to every row — "kg", "min".

Props

options required
readonly (PickerOption<T> | string)[]

The rows. Strings are shorthand for { value: s, label: s }.

value bindable

Defaults to undefined as T

T

The selected row's value. Bindable.

label
string

Accessible name for the wheel — "Hour", "Month". Required in practice.

caption
string

A caption above the wheel. Rarely needed; the value usually says it.

loop

Defaults to false

boolean

Spin past the ends and come back round. For a cycle — hours, minutes, months — and wrong for a bounded range like a year.

unit
string

A unit drawn small beside every row — "kg", "min".

size
PickerSize

Overrides the size inherited from the Picker.

sm
md
lg
visible
number

Overrides the visible row count inherited from the Picker.

disabled

Defaults to false

boolean
hug

Defaults to false

boolean

Let the wheel size itself to its widest row instead of sharing the width.

onchange
(value: T) => void

Fires once the wheel settles, not on every row it passes.

row
Snippet<[PickerOption<T>, boolean]>

Replaces the row's text. Gets the option and whether it is centred.

class
string
classes
PickerColumnClasses

Per-part Tailwind overrides. class still covers the root.

TimePicker

Hour, minute and AM/PM.

Always twelve-hour with AM/PM — omaris never shows a 24-hour clock. The value is still stored 24-hour — "14:30" — so it sorts, compares and goes into a database without a second format to agree on; that is data, and the wheels are what a person reads.

import { TimePicker } from 'omaris'
<TimePicker bind:value={opensAt} minuteStep={15} />

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
No description in the source yet.
header
The title line above the wheels.
stage
Everything the band is drawn behind: the wheels and both fades.
band
The strip marking the centre row.
wheels
The row of wheels. Masked so rows dissolve towards the edges.
actions
The cancel/confirm row. Only rendered when onconfirm is given.
action
One of those buttons.

Props

value bindable

Defaults to '09:00'

string

"HH:mm", stored 24-hour; the wheels always show 1–12 and AM/PM. Bindable.

minuteStep

Defaults to 1

number

Minutes between rows — 5 or 15 for an opening time, 1 for an alarm.

loop

Defaults to true

boolean

Spin past midnight and noon instead of stopping. On by default.

labels
{ hour?: string; minute?: string; meridiem?: string; am?: string; pm?: string }

Accessible names for the wheels, and the AM/PM row text.

onchange
(value: string) => void

Fires once a wheel settles, with the 24-hour string.

columnClasses
PickerColumnClasses

Per-part overrides for the wheels themselves.

classes
PickerClasses
size
PickerSize

Row height and type scale.

surface
PickerSurface

How the frame is drawn.

visible
number

Rows on screen at once. Rounded up to the next odd number.

title
string

A title above the wheels.

disabled
boolean

Disables every wheel inside.

haptics
boolean

Play a tick as each row passes the centre, once haptics are on.

onconfirm
() => void

Renders the action row. Called with the value already committed.

oncancel
() => void

Renders a cancel button beside it.

confirmLabel
string
cancelLabel
string
footer
Snippet

Replaces the built-in action row.

class
string