TimePicker

A time field with a list of slots.

Usage

import { TimePicker } from "@hoplallc/ui/time-picker";

TimePicker.Root opens a list of times rather than a clock. Picking from a list is faster than typing when the answer is one of a known set — appointment slots, opening hours, a shift start. When the time is arbitrary to the minute, the OS control is the better tool: TimePicker.Native lets someone type 14:37 without scrolling past it.

Three surfaces

The same three every picker family here has: Root is the composed popover, Native is the OS control, and Responsive renders Root on a pointer and Native under the useIsMobile breakpoint (768px).

The OS control

One field, both surfaces

One caveat specific to time: step shapes the desktop list only. The native control offers every minute whatever you pass, so a caller who needs the step enforced has to validate the value as well as bound the field.

The value is a "HH:mm" string, not a Date. A time with no date attached does not need one, and storing it as text avoids a timezone question that has no answer — 09:00 at a clinic is 09:00 wherever the reader happens to be.

Recipes

Opening hours, quarter-hour slots

min and max bound the list inclusively and step sets the spacing, so a clinic offering quarter-hour appointments between nine and half five is three props rather than a generated array. The list is built from these, which means an out-of-hours time cannot be picked — though, as ever, it can still arrive from stored data.

locale and formatOptions control how the times are displayed without touching what is stored: a French reader sees 14:30 and an American reader 2:30 PM, and both produce the string "14:30".

Set name and the field writes a hidden input carrying that same HH:mm, so a plain form post gets the value with no client code.

API

TimePicker.Root

PropTypeDefault
defaultOpenboolean—Whether the list starts open. Uncontrolled — for the controlled form use open.
defaultValuestring—Initial "HH:mm" value when uncontrolled.
formatOptionsDateTimeFormatOptions—Intl.DateTimeFormat options for the labels. Defaults to a short localized time ({ hour: "numeric", minute: "2-digit" }, e.g. "2:30 PM" / "14:30").
loadingboolean—Show a leading spinner and mark the button aria-busy, disabling interaction while an async action is in flight (e.g. a form submit).
localestring—BCP-47 locale (e.g. "fr-FR"), applied to the trigger and option labels.
maxstring"23:59"Last selectable time, "HH:mm" inclusive.
minstring"00:00"First selectable time, "HH:mm" inclusive.
namestring—Name for the hidden form input; the value is serialized as HH:mm.
onOpenChange((open: boolean) => void)—Called when the list opens or closes.
onValueChange((time: string | null) => void)—Called with the picked "HH:mm", or null when cleared. Never a Date.
openboolean—Controlled open state. Pair it with onOpenChange.
placeholderstring"Pick a time"Shown on the trigger while nothing is selected.
shape"default" | "pill"—pill forces a fully rounded button; default follows the theme's --radius, so it changes with the brand.
size"default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs"—Height and horizontal padding. The icon-* set is square, for a button whose entire content is one icon — give those an aria-label.
stepnumber30Minutes between two options. Defaults to 30.
valuestring | null—Controlled "HH:mm" value; pass null for a controlled empty selection.
variant"brand" | "default" | "destructive" | "ghost" | "highlight" | "link" | "outline" | "primary" | "secondary"—Weight of the action. default is the one thing the page exists for, at most one per view; secondary an equal-weight alternative; outline for toolbars and anything that repeats in a row; ghost inside dense surfaces; destructive for what cannot be undone; link for an action that reads as navigation.
exitExit—Inside a <Presence>: plays this child's way out before it unmounts.
classNamestring | ((state: ButtonState) => string | undefined)—CSS class applied to the element, or a function that returns a class based on the component's state.
focusableWhenDisabledboolean—Whether the button should be focusable when disabled.
nativeButtonboolean—Whether the component renders a native <button> element when replacing it via the render prop. Set to false if the rendered element is not a button (for example, <div>).
renderComponentRenderFn<HTMLProps, ButtonState> | ReactElement<unknown, string | JSXElementConstructor<any>>—Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleCSSProperties | ((state: ButtonState) => CSSProperties | undefined)—Style applied to the element, or a function that returns a style object based on the component's state.

Plus the DOM props of the element it renders. Source: time-picker/components/time-picker-root.tsx.

TimePicker.Native

PropTypeDefault
classNamestring—Classes for the field's box, so className means the same thing here as on the popover surfaces. The inner <input> keeps the kit's field styling; reach it through inputClassName on the rare occasion you must.
inputClassNamestring—Classes for the inner <input> itself.
onValueChange((time: string | null) => void)—Called with the picked "HH:mm", or null when the field is emptied.
size"default" | "sm"—Field height. sm for dense surfaces such as table cells and toolbars.
valuestring | null—Controlled "HH:mm" value; pass null for a controlled empty selection.
exitExit—Inside a <Presence>: plays this child's way out before it unmounts.

Plus the DOM props of the element it renders. Source: time-picker/components/native-time-input.tsx.

TimePicker.Responsive

PropTypeDefault
classNamestring—Classes for the field itself: the popover trigger on desktop, the wrapper around the native input on mobile. A plain string only — the trigger's Button accepts a state function, but a native input has no state to read.
defaultValuestring—Initial "HH:mm" value when uncontrolled.
formatOptionsDateTimeFormatOptions—Intl.DateTimeFormat options for the labels. Defaults to a short localized time ({ hour: "numeric", minute: "2-digit" }, e.g. "2:30 PM" / "14:30").
localestring—BCP-47 locale (e.g. "fr-FR"), applied to the trigger and option labels.
maxstring—Last selectable time, "HH:mm" inclusive.
minstring—First selectable time, "HH:mm" inclusive.
namestring—Name for the hidden form input; the value is serialized as HH:mm.
onValueChange((time: string | null) => void)—Called with the picked "HH:mm", or null when cleared. Never a Date.
placeholderstring—Shown on the trigger while nothing is selected.
stepnumber—Minutes between two options. Defaults to 30.
valuestring | null—Controlled "HH:mm" value; pass null for a controlled empty selection.

Plus the DOM props of the element it renders. Source: time-picker/components/responsive-time-input.tsx.

DateTimePicker · NativeDatePicker · DatePicker · Field