DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
CalendarDate PickerTime PickerTime RangeTime Slots
AI agentsContributing
  1. Components
  2. Time Picker

Time Picker

Four time-picker variants — a drum-roll 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.

$ pnpm add @tessinaui/ui

When to use

  • A time of day. Hours and minutes, with optional seconds, as a 12h or 24h display over an always-24h value.
  • A duration. mode="duration" takes a span rather than a clock time, with unit labels and no AM/PM.
  • Times people know and type. variant="input" suits dense forms and keyboard-first flows.
  • Times chosen by scrolling. variant="wheel" fits a view where picking the time is the point and there is room for a drum that stays visible, inline or in a bottom sheet.
  • A form row with room for only a field. variant="popover" opens the wheel on demand.
  • Times on a fixed grid. variant="list" gives a scannable list with type-ahead in a desktop form.
  • A bounded window. minTime, maxTime and per-unit steps keep every choice legal, such as business hours or quarter-hour slots.

When not to use

  • A start and an end time. Use TimeRange, which shares one wheel between the two ends.
  • Availability slots. Chip grids, delivery windows and ASAP rows are Time Slots.
  • A date, or a date with a time. Use DatePicker or Calendar.
  • A handful of fixed named times. Use Select.
  • A time zone. Use Select or Combobox.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { TimePicker } from "@tessinaui/ui";
example.tsx
{/* 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 must divide the hour cycle (12 or 24); minutesStep and secondsStep must divide 60, up to 30. 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. A focused segment becomes the plate's inverse chip — a native time field's selected segment — because no wash on the well carries its digits at AA.

Locales and RTL

Column/segment order follows the locale (Intl.formatToParts), never the page direction. Leave dir unset and the chrome, segment arrow keys and popup follow the page's direction; only an explicit dir="rtl" defaults the strings to Arabic, since an inherited direction does not name a language.

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.

Error

errorMessage explains why a time is invalid. It renders below the control in a polite live region mounted from first render, joins the message into the focus host's aria-describedby — each segment or wheel column, the list combobox, or the popover trigger — and marks the host aria-invalid where its role allows it (a button does not, so the popover trigger carries data-invalid). It implies intent="error". Type a time outside opening hours here; use minTime/maxTime instead when such a time should never be enterable.

Skeleton

Variants

VariantBehaviour
wheelInline drum-roll columns — always visible; snap-scroll, drag, tap, and full keyboard
inputTyped HH / MM (/ SS) spinbutton segments and an AM/PM toggle for 12h
popoverField-shaped trigger that opens the wheel in a named popup (focus lands on the hours column)
listField-shaped trigger that opens a generated listbox of times across the window — minutesStep defaults to 30 here

API Reference

PropTypeDefaultDescription
variant"wheel" | "input" | "popover" | "list""wheel"Display mode
mode"time" | "duration""time"Duration = 0–23h span, no AM/PM, unit labels forced
valuestring | null—Controlled value in "HH:mm" or "HH:mm:ss" 24h format
defaultValuestring | nullnullUncontrolled 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
showSecondsbooleanfalseSeconds column / segment
hoursStepnumber1Hour interval — must divide the hour cycle
minutesStepnumber1 (list: 30)Minute interval — a divisor of 60, max 30
secondsStepnumber1Second interval — a divisor of 60, max 30
step1 | 5 | 10 | 15 | 30—Deprecated alias for minutesStep + secondsStep (one minor)
minTime / maxTimestring—Window bounds, "HH:mm[:ss]". An inverted window is ignored with a dev warning
shouldDisableTime(value, unit) => boolean—Disable specific times per unit
hideDisabledOptionsbooleanfalselist only: drop disabled options instead of rendering them disabled
presetsstring[] | {label, values}[]—Quick-pick chips in the popover/list panels
showNowbooleanfalse"Now" chip — current time snapped to steps, clamped to the window
clearablebooleanfalseClear 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)
errorMessageReactNode—Error text below the control. Announced politely, joined into the focus host's aria-describedby, marks it invalid; implies intent="error" unless another intent is set. Hidden while readOnly
rounded"none" | "sm" | "md" | "lg" | "full""full"Shells use the control scale (full = pill); wheel/popup panels cap full at rounded-3xl
tone"default" | "on-color"inheritedOn-color well recipe on coloured surfaces
width"narrow" | "default" | "wide""default"Wheel column width preset
borderedbooleantrueWheel only: paint the drum's own plate (false when a panel already provides one)
disabledbooleanfalseToken-swap disable (no opacity fade)
readOnlybooleanfalseValue locked; popover/list stay closed, focus preserved
dir"ltr" | "rtl"inheritedChrome direction — column order comes from locale, not from this. Unset, read off the page
localestring"en-US" ("ar" when rtl)Decides segment/column ORDER and AM/PM glyphs
stringsPartial<TimePickerStrings>—Override any built-in label/aria string
placeholderstring"--:-- --" / "--:--"Empty display in triggers
namestring—Serializes the value via a hidden input
open / defaultOpen / onOpenChange——Controlled popover/list open state
skeletonbooleanfalseShape-accurate placeholder — forwards format/seconds/labels/width
showLabelsbooleanfalseStationary hours/min/sec labels beside the selected numbers
classNamestring—Classes on the root wrapper

Accessibility

VoiceOver + Safari: passed 2026-09-14. The wheel reads "Time, group", then "Hours, list box, 10 selected (10 of 12)"; Down reads "11 selected (11 of 12)"; Tab moves to minutes, "30 selected (31 of 60)". NVDA and JAWS are untested.

  • Drum columns are role="listbox" with one tab stop each, aria-activedescendant tracking the active row, aria-selected on the committed option, and aria-disabled on 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" with aria-valuenow/min/max/text (a localized "Empty" while placeholder), typed digits with early auto-advance, Backspace digit-strip then previous-segment, ArrowLeft/Right between segments (visually flipped in RTL), Home/End to min/max, and A/P on the period toggle. On iOS the role flips to textbox — VoiceOver there cannot focus spinbuttons (a deliberate platform 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"). format only 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; dir mirrors surrounding chrome only, and digits render as a bidi-isolated LTR run.
  • Boundaries — a start–end pair sharing one wheel is TimeRange (two TimePickers side by side is the composition when the ends are independent); availability slots (chip grids, delivery windows, day strips, ASAP rows) are a separate component — Time Slots; date+time together is DatePicker/Calendar's job (Calendar composes this wheel); circular bedtime-style range dials compose CircularSlider; timezone selection composes Select/Combobox.
  • Wheel metrics — drum rows keep the platform 32–48px heights (scroll-first surfaces). Columns are at least 44px wide except at the dense steps: xs (40px default, 36px narrow) and narrow sm (40px). Shell variants carry the standard 44px touch floors, the clear button included.
  • A drum never shrinks — a labelled (showLabels, duration) or xl drum needs its full width. In a narrower column use width="narrow", a smaller size, or the popover/input variant.
Date PickerTime Range

On this page

When to useWhen not to useInstallationUsageExamplesDefaultVariantsSizes, width and labelsFormatsWith SecondsMin / max windowPer-unit stepsDisabled timesList modeDurationPresets and NowClearableIn a formOn colorLocales and RTLLive captionBottom sheetIntentsStatesErrorSkeletonVariantsAPI ReferenceAccessibilityNotes