Skip to content

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.

On this page

Example

Time Picker.tsxtsx
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.

  1. Label

    Visible name connected to the TimeField. Owns a DOM element.

  2. TimeField

    Native editable time input and form control. Owns a DOM element.

  3. Popover

    Wrapper-free provider for the suggested-time popover. Does not add a DOM element.

  4. PopoverTrigger

    Icon button that opens the time suggestions. Owns a DOM element.

  5. PopoverOverlay

    Floating surface for the suggested times. Owns a DOM element.

  6. ListBox

    Selectable collection synchronized with the TimeField. Owns a DOM element.

  7. ListBoxItem

    One localized label backed by an HH:mm value. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Keep a labelled TimeField as the editable source of truth.

  2. 2

    Add the supporting parts

    Open a ListBox of times from a PopoverTrigger beside the field.

  3. 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

Importtsx
import { Label, ListBox, ListBoxItem, Popover, PopoverOverlay, PopoverTrigger, TimeField } from "@comp0/react";

Label

DOM element

Visible name connected to the TimeField.

TimeField

DOM element

Native editable time input and form control.

PropTypeDescription
valuestringControlled time as HH:mm.
namestringSubmission name for the time value.
onChange(event) => voidReceives typed native input changes.

Popover

Context only

Wrapper-free provider for the suggested-time popover.

PopoverTrigger

DOM element

Icon button that opens the time suggestions.

PropTypeDescription
aria-labelstringNames the icon-only trigger.

PopoverOverlay

DOM element

Floating surface for the suggested times.

PropTypeDescription
placementPopoverPlacementPlaces the surface beside the trigger.
offsetnumberPixel gap between the trigger and surface.

ListBox

DOM element

Selectable collection synchronized with the TimeField.

ListBoxItem

DOM element

One 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.

Keep exploring