Skip to content
garn

TimePicker

A time field with a scrollable list of selectable time slots — the booking-UI pattern.

Stablev0.4.2added in v0.2.0@garn/ui/time-picker
On this page

Default

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.

terminal
npx garn-ui add time-picker
First time? Set up garn in your project
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

SelectorState
:hoverhover
:focusfocus
:focus-visiblefocus-visible
[aria-invalid=true]invalid
[disabled]disabled
[aria-selected=true]slot-selected
PropertyTokenTier
height--garn-control-h-mdsemantic · air
paddingInline--garn-control-px-mdsemantic · 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

Do
  • Set interval to 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
  • 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

warn

Give every time picker an accessible label.

when no associated <label htmlFor>, aria-label, or aria-labelledby

Keyboard

ArrowDownFrom the field: open the slot list. In the list: move the roving focus to the next slot (clamped at the end, no wrap).
ArrowUpIn the list: move the roving focus to the previous slot (clamped at the start, no wrap).
HomeIn the list: jump the roving focus to the first slot.
EndIn the list: jump the roving focus to the last slot.
PageDownIn the list: jump the roving focus forward ~one hour of slots.
PageUpIn the list: jump the roving focus back ~one hour of slots.
EnterFrom the field: commit the typed time. In the list: commit the focused slot (select + close + return focus to the field).
SpaceIn the list: commit the focused slot (select + close + return focus to the field).
EscapeClose the slot list.
A1.3.1Info and RelationshipsA2.1.1KeyboardA4.1.2Name, Role, Value
  • 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.