Pickers and overlays
Time Picker
A typed time input with a custom list of useful times.
When to use it: Use this composition when people may type an exact time but a short set of common times speeds up selection.
Example
import { useState } from "react";
import {
Label,
ListBox,
ListBoxItem,
Popover,
PopoverOverlay,
PopoverTrigger,
TimeField,
} from "@comp0/react";
import { ClockIcon } from "@heroicons/react/16/solid";
const times = [
["09:00", "9:00 AM"],
["09:30", "9:30 AM"],
["10:00", "10:00 AM"],
["10:30", "10:30 AM"],
["11:00", "11:00 AM"],
] as const;
export function Example() {
const [open, setOpen] = useState(false);
const [time, setTime] = useState("09:00");
return (
<div className="flex max-w-xs flex-col gap-1.5">
<Label
className="text-base font-medium text-zinc-900 sm:text-sm dark:text-zinc-100"
htmlFor="meeting-time"
>
Meeting time
</Label>
<div className="flex gap-1.5">
<TimeField
id="meeting-time"
name="meetingTime"
value={time}
onChange={(event) => setTime(event.currentTarget.value)}
className="w-full rounded border border-zinc-950/10 bg-white px-3 py-2.5 text-base text-zinc-950 outline-teal-600 focus-visible:outline-2 sm:py-2 sm:text-sm dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-50 dark:outline-teal-400"
/>
<Popover open={open} onToggle={setOpen}>
<PopoverTrigger
aria-label="Choose time"
className="rounded border border-zinc-950/10 bg-white px-3 text-zinc-700 outline-teal-600 focus-visible:outline-2 data-open:bg-zinc-100 dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-300 dark:outline-teal-400 dark:data-open:bg-zinc-800"
>
<ClockIcon className="size-4" aria-hidden="true" />
</PopoverTrigger>
<PopoverOverlay
aria-label="Available times"
placement="bottom end"
offset={4}
className="w-40 rounded border-0 bg-white p-1 opacity-100 shadow-lg ring-1 ring-zinc-950/10 transition-[opacity,translate] duration-150 ease-out starting:-translate-y-1 starting:opacity-0 motion-reduce:transition-none dark:bg-zinc-900 dark:shadow-none dark:ring-white/10"
>
<ListBox
aria-label="Available times"
className="grid gap-1"
value={time}
onChange={(nextTime) => {
setTime(nextTime);
setOpen(false);
}}
>
{times.map(([value, label]) => (
<ListBoxItem
key={value}
value={value}
className="cursor-pointer rounded px-3 py-2.5 text-base text-zinc-800 data-selected:bg-teal-100 data-selected:text-teal-950 focus-visible:bg-teal-200 data-selected:focus-visible:bg-teal-200 focus-visible:outline-2 focus-visible:outline-teal-600 sm:py-2 sm:text-sm dark:text-zinc-100 dark:data-selected:bg-teal-950 dark:data-selected:text-teal-50 dark:focus-visible:bg-teal-800 dark:data-selected:focus-visible:bg-teal-800 dark:focus-visible:outline-teal-400"
>
{label}
</ListBoxItem>
))}
</ListBox>
</PopoverOverlay>
</Popover>
</div>
</div>
);
}Anatomy
Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.
A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.
Label
Visible name connected to the TimeField. Owns a DOM element.
TimeField
Native editable time input and form control. Owns a DOM element.
Popover
Wrapper-free provider for the suggested-time popover. Does not add a DOM element.
PopoverTrigger
Icon button that opens the time suggestions. Owns a DOM element.
PopoverOverlay
Floating surface for the suggested times. Owns a DOM element.
ListBox
Selectable collection synchronized with the TimeField. Owns a DOM element.
ListBoxItem
One localized label backed by an HH:mm value. Owns a DOM element.
Step by step
- 1
Add the main part
Keep a labelled TimeField as the editable source of truth.
- 2
Add the supporting parts
Open a ListBox of times from a PopoverTrigger beside the field.
- 3
Make the behavior clear
Synchronize list selection with the field and close the popover after a choice.
Exampletsx <Popover><PopoverTrigger aria-label="Choose time" /><PopoverOverlay><ListBox aria-label="Available times"><ListBoxItem value="09:00">9:00 AM</ListBoxItem></ListBox></PopoverOverlay></Popover>
Keyboard
- ↵
- Opens from the trigger or selects the focused time.
- Space
- Opens from the trigger or selects the focused time.
- Esc
- Closes the suggestions and returns focus.
- ↑↓
- Moves through the suggested times.
Forms and accessibility
TimeField remains the native form control; give it a name and it submits the synchronized HH:mm value.
Accessibility checklist
- Keep the TimeField editable so the suggested list is never the only way to enter a time.
- Name the icon-only trigger and the list of suggested times separately.
- Format visible options for the locale while keeping stable HH:mm values for the field and form.
API reference
import { Label, ListBox, ListBoxItem, Popover, PopoverOverlay, PopoverTrigger, TimeField } from "@comp0/react";Label
DOM elementVisible name connected to the TimeField.
TimeField
DOM elementNative editable time input and form control.
| Prop | Type | Description |
|---|---|---|
value | string | Controlled time as HH:mm. |
name | string | Submission name for the time value. |
onChange | (event) => void | Receives typed native input changes. |
Popover
Context onlyWrapper-free provider for the suggested-time popover.
PopoverTrigger
DOM elementIcon button that opens the time suggestions.
| Prop | Type | Description |
|---|---|---|
aria-label | string | Names the icon-only trigger. |
PopoverOverlay
DOM elementFloating surface for the suggested times.
| Prop | Type | Description |
|---|---|---|
placement | PopoverPlacement | Places the surface beside the trigger. |
offset | number | Pixel gap between the trigger and surface. |
ListBox
DOM elementSelectable collection synchronized with the TimeField.
ListBoxItem
DOM elementOne localized label backed by an HH:mm value.
Style hooks
Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.
[data-open]on PopoverTrigger
- The suggestions are open.
[data-selected]on ListBoxItem
- The field uses this time.
[data-value]on TimeField
- The field has a time value.