Time Picker
Four time-picker variants — an iOS-style drum wheel, a typed spinbutton segment field, a popover trigger, and a generated time list — with 12h/24h formats, duration mode, min/max windows, per-unit steps, presets, and full keyboard and screen-reader contracts.
Playground
Installation
pnpm add @tessinaui/uiUsage
import { TimePicker } from "@tessinaui/ui";{/* Inline wheel (default) */}
<TimePicker defaultValue="10:30" />
{/* Typed spinbutton segments */}
<TimePicker variant="input" defaultValue="10:30" />
{/* Field trigger + wheel popover */}
<TimePicker variant="popover" defaultValue="10:30" />
{/* Generated time list (desktop/enterprise mode, 30-min default step) */}
<TimePicker variant="list" defaultValue="10:30" />
{/* Business-hours window */}
<TimePicker minTime="09:00" maxTime="17:00" defaultValue="09:30" />
{/* Duration (0–23h span, no AM/PM, unit labels) */}
<TimePicker mode="duration" defaultValue="00:30" />
{/* Controlled */}
<TimePicker value={time} onChange={setTime} />Examples
Default
An inline drum-roll wheel — the simplest usage.
Variants
Four display modes — wheel, input, popover, and list.
Sizes, width and labels
Shells run the Field ladder (32/36/40/48/56); the drum keeps the platform wheel metric. width widens columns, showLabels adds stationary unit labels.
Formats
12-hour (AM/PM) and 24-hour display with the format prop. The emitted value is always 24h.
With Seconds
Add a seconds column with showSeconds.
Min / max window
minTime/maxTime bound the pickable window: wheel rows stay visible as disabled options and spring back, segments clamp on commit, the list truncates.
Per-unit steps
hoursStep / minutesStep / secondsStep — each must divide 60 (the UIDatePicker rule); illegal values fall back to 1 with a dev warning.
Disabled times
shouldDisableTime(value, unit) blocks specific times; the list can drop them with hideDisabledOptions.
List mode
The generated time list — typeahead comes from the Select primitive: focus the trigger and type.
Duration
mode="duration" — a 0–23h span with forced unit labels and no AM/PM. Same "HH:mm[:ss]" wire value.
Presets and Now
Quick-pick chips (flat or grouped) plus a computed "Now" chip.
Clearable
A clear affordance while a value is set. onClear notifies controlled consumers.
In a form
name serializes the value through a hidden input for native form posts.
On color
The shells switch to the shared on-color well recipe; tone inherits from the nearest Surface.
Locales and RTL
Column/segment order follows the locale (Intl.formatToParts), never the page direction.
Live caption
The derived-value caption (alarm apps' "rings in…") is a composition, not a prop.
Bottom sheet
The mobile sheet presentation composes the house Drawer — the sheet is the plate (bordered={false}) and its footer CTA owns the commit.
Intents
Colour the selection band and field via the intent prop.
States
Read-only, disabled, and the shape-accurate loading skeleton.
Variants
| Variant | Behaviour |
|---|---|
wheel | Inline drum-roll columns — always visible; snap-scroll, drag, tap, and full keyboard |
input | Typed HH / MM (/ SS) spinbutton segments and an AM/PM toggle for 12h |
popover | Field-shaped trigger that opens the wheel in a named popup (focus lands on the hours column) |
list | Field-shaped trigger that opens a generated listbox of times across the window — minutesStep defaults to 30 here |
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "wheel" | "input" | "popover" | "list" | "wheel" | Display mode |
mode | "time" | "duration" | "time" | Duration = 0–23h span, no AM/PM, unit labels forced |
value | string | null | — | Controlled value in "HH:mm" or "HH:mm:ss" 24h format |
defaultValue | string | null | null | Uncontrolled initial value |
onChange | (value: string) => void | — | Fires with the canonical 24h string |
onClear | () => void | — | Fires when the clear affordance empties the value |
format | "12h" | "24h" | "12h" | Display format only — the value stays 24h |
showSeconds | boolean | false | Seconds column / segment |
hoursStep | number | 1 | Hour interval — must divide the hour cycle |
minutesStep | number | 1 (list: 30) | Minute interval — a divisor of 60, max 30 |
secondsStep | number | 1 | Second interval — a divisor of 60, max 30 |
step | 1 | 5 | 10 | 15 | 30 | — | Deprecated alias for minutesStep + secondsStep (one minor) |
minTime / maxTime | string | — | Window bounds, "HH:mm[:ss]". An inverted window is ignored with a dev warning |
shouldDisableTime | (value, unit) => boolean | — | Disable specific times per unit |
hideDisabledOptions | boolean | false | list only: drop disabled options instead of rendering them disabled |
presets | string[] | {label, values}[] | — | Quick-pick chips in the popover/list panels |
showNow | boolean | false | "Now" chip — current time snapped to steps, clamped to the window |
clearable | boolean | false | Clear affordance while a value is set |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Density — shells run the Field ladder, drums the wheel metric |
intent | "none" | "error" | "warning" | "success" | "info" | "none" | Semantic colour. "primary" is a deprecated alias (one minor) |
rounded | "none" | "sm" | "md" | "lg" | "full" | "md" | Shells use the control scale (full = pill); wheel/popup panels cap full at rounded-3xl |
tone | "default" | "on-color" | inherited | On-color well recipe on coloured surfaces |
width | "narrow" | "default" | "wide" | "default" | Wheel column width preset |
bordered | boolean | true | Wheel only: paint the drum's own plate (false when a panel already provides one) |
disabled | boolean | false | Token-swap disable (no opacity fade) |
readOnly | boolean | false | Value locked; popover/list stay closed, focus preserved |
dir | "ltr" | "rtl" | "ltr" | Chrome direction — column order comes from locale, not from this |
locale | string | "en-US" ("ar" when rtl) | Decides segment/column ORDER and AM/PM glyphs |
strings | Partial<TimePickerStrings> | — | Override any built-in label/aria string |
placeholder | string | "--:-- --" / "--:--" | Empty display in triggers |
name | string | — | Serializes the value via a hidden input |
open / defaultOpen / onOpenChange | — | — | Controlled popover/list open state |
skeleton | boolean | false | Shape-accurate placeholder — forwards format/seconds/labels/width |
showLabels | boolean | false | Stationary hours/min/sec labels beside the selected numbers |
className | string | — | Classes on the root wrapper |
Accessibility
- Drum columns are
role="listbox"with one tab stop each,aria-activedescendanttracking the active row,aria-selectedon the committed option, andaria-disabledon out-of-window rows (visible, non-committable, sprung back from). Keyboard:ArrowUp/Down±1,PageUp/Down±5,Home/End— all seek past disabled rows. - Segments are
role="spinbutton"witharia-valuenow/min/max/text(a localized "Empty" while placeholder), typed digits with early auto-advance,Backspacedigit-strip then previous-segment,ArrowLeft/Rightbetween segments (visually flipped in RTL),Home/Endto min/max, andA/Pon the period toggle. On iOS the role flips totextbox— VoiceOver there cannot focus spinbuttons (the React Aria carve-out). - Popover popups are named, focus the hours column on open, close on
Enter/Escape, and return focus to the trigger. The inner wheel demotes to presentation so the widget announces once. aria-*passed to the root lands on the element carrying the role (§7 split);data-*stays on the wrapper.
Notes
- Value format — always stored and emitted in 24h
"HH:mm"(or"HH:mm:ss").formatonly changes the display; duration mode shares the same wire format (≤23:59). - Commit model — inline surfaces apply instantly (the platform consensus); the drum commits ~100ms after the scroll settles. A confirmation footer belongs to a sheet/dialog composition (see Bottom sheet), not to the component.
- Locale, not direction — segment and column order come from the locale's
Intl.formatToParts;dirmirrors surrounding chrome only, and digits render as a bidi-isolated LTR run. - Boundaries — availability slots (chip grids, delivery windows, day strips, ASAP rows) are a separate component (see the
time-slotstracker); date+time together isDatePicker/Calendar's job (Calendar composes this wheel); circular bedtime-style range dials composeCircularSlider; timezone selection composesSelect/Combobox. - Wheel metrics — drum rows keep the platform 32–48px heights (scroll-first surfaces; each column is a ≥44px-wide touch target). Shell variants carry the standard phone floors.