Time Picker
A keyboard-first time input with 12/24-hour formats and step granularity.
Locale format
09:30
24-hour, with seconds
null
Outside business hours
Installation
Usage
Segmented hour, minute, and optional second fields that behave the way a native date field does —
type digits and the caret advances, arrows step, each segment announces itself — without inheriting
<input type="time">'s unstyleable UI or its inconsistent behavior across browsers.
import { TimePicker } from "@/components/ui/time-picker";
export function ReminderField() {
const [time, setTime] = React.useState<string | null>("09:30");
return <TimePicker value={time} onValueChange={setTime} />;
}The value is always 24-hour "HH:mm" regardless of what is displayed, and null until every
segment is filled. That is the form a time column, an ISO timestamp, and a <input type="hidden">
all want.
Hour cycle
Display follows the viewer's locale by default — a US visitor sees 09:30 AM, a French visitor sees
09:30 — while the value stays 24-hour. Force it when the domain demands one format:
<TimePicker hourCycle={24} withSeconds />The locale is only readable on the client, so a server render cannot know it. Left unset, the field
renders 24-hour on the server and switches to the locale's cycle once mounted — a 12-hour visitor
sees the AM/PM segment appear on hydration. Pass hourCycle explicitly to render the same markup on
both sides and avoid that shift.
Stepping
step is the minute increment for the arrow keys. Typing digits is never restricted by it, so a
15-minute grid still permits an exact 09:07 when someone types it.
<TimePicker step={15} />Range
min and max mark the field invalid — data-invalid for styling, aria-invalid on the segments —
rather than silently rewriting what the user typed. Clamping input as it is typed makes intermediate
values impossible to reach.
<TimePicker min="09:00" max="17:00" />Props
| Prop | Description |
|---|---|
value / defaultValue / onValueChange | Controlled or uncontrolled "HH:mm" string, or null. |
hourCycle | 12 or 24. Defaults to the viewer's locale, resolved after mount. |
step | Minute increment for the arrow keys. Defaults to 1. |
withSeconds | Add a seconds segment; the value becomes "HH:mm:ss". |
min / max | Flag values outside the range as invalid. |
readOnly | Focusable and announced, but not editable. |
name | Emits a hidden input so the field posts in a plain HTML form. |
Keyboard
| Key | Action |
|---|---|
↑ / ↓ | Step the focused segment, wrapping at its bounds. |
← / → | Move between segments. |
0–9 | Type directly. The caret advances as soon as no further digit fits — 3 in a 24-hour hour segment moves on immediately, 1 waits for a second digit. |
Backspace / Delete | Clear the segment. |
A / P | Set AM or PM on the day-period segment. |
Each segment is a spinbutton with its own label and value text, so a screen reader announces
"hour, 9" rather than reading the whole field as one opaque string. Empty segments read as "Empty"
instead of "minus minus".
Composing a range
Two pickers and a date range picker make a datetime range without another component:
<DateRangePicker value={range} onValueChange={setRange} />
<TimePicker value={startTime} onValueChange={setStartTime} aria-label="Start time" />
<TimePicker value={endTime} onValueChange={setEndTime} aria-label="End time" />