TimePicker
A time field with a scrollable list of selectable time slots — the booking-UI pattern.
@garn/ui/time-pickerDefault
A time field with a 30-minute slot list.
Interval
A 15-minute slot interval.
Twelve Hour
A 12-hour format (h:mm a).
States
Disabled and invalid.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add time-picker- Registry
time-picker- Deps
date-fnsclass-variance-authoritylucide-react- Registry deps
utilsdate-iofield-surfaceuse-controllable-statedate-picker
Import
import { TimePicker } from "@garn/ui/time-picker";Props
The component's public props and their types.
appearance- Description
- No description.
- Type
"ghost" | "outline" | "soft"- Default
outline
size- Description
- No description.
- Type
"lg" | "md" | "sm" | "xl" | "xs"- Default
md
"aria-invalid"- Description
- No description.
- Type
"false" | "true" | boolean
"aria-label"- Description
- No description.
- Type
string
"aria-labelledby"- Description
- No description.
- Type
string
className- Description
- No description.
- Type
string
clearable- Description
- Show a clear button when a time is set.
- Type
boolean- Default
true
clearLabel- Description
- Accessible name for the clear button.
- Type
string- Default
"Clear time"
contentClassName- Description
- No description.
- Type
string
defaultOpen- Description
- Whether the popover starts open (uncontrolled).
- Type
boolean- Default
false
defaultValue- Description
- Initial time (uncontrolled).
- Type
Date
disabled- Description
- Disable the field and block opening.
- Type
boolean- Default
false
format- Description
- date-fns token string driving display, parse, and the placeholder.
- Type
string- Default
"HH:mm"
id- Description
- No description.
- Type
string
interval- Description
- Minutes between slots in the list.
- Type
number- Default
30
name- Description
- Name for a hidden input mirroring the value on form submit as a plain `HH:mm:ss` time of day (the `<input type="time" step="1">` wire format — never an invented-date UTC instant).
- Type
string
onOpenChange- Description
- Fires when the popover opens or closes.
- Type
(open: boolean) => void
onValueChange- Description
- Fires with the committed time (or `undefined` when cleared).
- Type
(value: Date | undefined) => void
open- Description
- Popover open state (controlled).
- Type
boolean
placeholder- Description
- Field text when nothing is selected (defaults to the `format` string).
- Type
string
readOnly- Description
- Render read-only — no editing or opening.
- Type
boolean- Default
false
ref- Description
- No description.
- Type
React.Ref<HTMLInputElement>
required- Description
- Mark the field required for form validation.
- Type
boolean
triggerLabel- Description
- Accessible name for the trigger that opens the time list.
- Type
string- Default
"Open time list"
value- Description
- Selected time as a `Date` (time portion meaningful; date portion preserved).
- Type
Date
Plus 3 inherited native <input> attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="time-picker"][data-slot="time-picker-input"][data-slot="time-picker-trigger"][data-slot="time-picker-clear"][data-slot="time-picker-option"]States
| Selector | State | Description |
|---|---|---|
:hover | hover | Field edge / fill grows stronger; slots highlight. |
:focus | focus | Border darkens on any focus. |
:focus-visible | focus-visible | Keyboard focus adds a neutral ring. |
[aria-invalid=true] | invalid | Danger border (+ danger ring on focus). |
[disabled] | disabled | Dims and blocks typing; the toggle disables too. |
[aria-selected=true] | slot-selected | The slot matching the current value. |
| Property | Token | Tier |
|---|---|---|
height | --garn-control-h-md | semantic · air |
paddingInline | --garn-control-px-md | semantic · air |
When to use
Pick a time from fixed slots (appointment, reservation) while still allowing free typing.
Reach for something else when
- A bare typed time (use TimeField)
- a date + time (use DateTimePicker)
- an arbitrary-precision duration.
Overview
TimePicker is TimeField's pick-from-a-list sibling — the booking-UI pattern. It's the same typeable field, plus a Popover holding a scrollable listbox of time slots at your chosen interval (default 30 min). It reuses the date family's field + popover shell (with a clock trigger), so it lines up with DatePicker and the rest of the form surface. The value is a native Date; picking a slot preserves the value's date, and typing stays available alongside the list.
Use TimeField when a bare typed time is enough (no slots), and DateTimePicker for a date and time together.
Guidelines
Choose an interval that matches the booking granularity (15/30 min). Pair with a Label and pick a 24h/12h format. The typed field is fully keyboard-operable (ArrowDown opens, Enter commits, Escape closes); the slot list is keyboard-operable too (arrows/Home/End/Page roving focus, Enter/Space commits), with the current time marked and scrolled into view on open. Reflect validity with aria-invalid.
Wire format: HH:mm:ss. With name, the hidden form input posts the canonical time of day (the <input type="time" step="1"> wire format) — a time-only field never posts an invented-date UTC instant.
Best practices
- Set
intervalto the slot granularity; pair with a label. - Hold the value in state; read it as a
Date. - Reflect validation with aria-invalid + a described message.
- Don't rely on the placeholder as the label.
- Don't use huge slot lists at 1-minute intervals — prefer TimeField for fine typing.
Content guidelines
- Label with what the time is for ("Appointment time").
Troubleshooting
The slot list is enormous.
Cause. A small interval over 24 hours yields many options (e.g. 96 at 15 min).
Fix. Use a coarser interval, or switch to TimeField for free typing.
Two slots show the same label (e.g. 01:00 appears twice).
Cause. A 12-hour format without a meridiem token (e.g. hh:mm) renders the AM and PM slots identically.
Fix. Include a meridiem token in format (e.g. h:mm a) so the 12-hour slots read unambiguously.
Accessibility
- Role
textbox- ARIA APG
- combobox
- Focus
- Field focus surface as the date pickers; the list is opened from the field and dismissed on Escape/outside.
Accessibility requirements
Give every time picker an accessible label.
when no associated <label htmlFor>, aria-label, or aria-labelledby
Keyboard
| ArrowDown | From the field: open the slot list. In the list: move the roving focus to the next slot (clamped at the end, no wrap). |
| ArrowUp | In the list: move the roving focus to the previous slot (clamped at the start, no wrap). |
| Home | In the list: jump the roving focus to the first slot. |
| End | In the list: jump the roving focus to the last slot. |
| PageDown | In the list: jump the roving focus forward ~one hour of slots. |
| PageUp | In the list: jump the roving focus back ~one hour of slots. |
| Enter | From the field: commit the typed time. In the list: commit the focused slot (select + close + return focus to the field). |
| Space | In the list: commit the focused slot (select + close + return focus to the field). |
| Escape | Close the slot list. |
- Give the field an accessible label; the format-pattern placeholder is not a label.
- The slot list is a role=listbox of role=option slots; the current time carries aria-selected and scrolls into view on open.
- The slot list is fully keyboard-operable: a roving tabindex keeps exactly one option tabbable at a time; arrows/Home/End/PageUp/PageDown move the roving focus, and Enter/Space commits the focused slot. The list is opened from the field and returns focus there on commit.
- Type-ahead (printable-character jump within the list) is intentionally omitted — the typed field is the primary character-entry path; a deliberate deviation from the APG Listbox pattern.
- Reflect validation with aria-invalid + a described message.