Gesso
Forms

Time picker

A time field that opens the day's times in a Popover and closes on the pick.

Time picker is a time field: a button that shows the chosen time and opens a list of the day's times in a Popover to change it. It is Date picker's companion and looks like it, so a date and a time can sit side by side. Picking a time sets the value and closes the popover, and focus goes back to the button.

Usage

'use client';

import * as React from 'react';
import { Field, FieldLabel } from '@/components/ui/field';
import { TimePicker } from '@/components/ui/time-picker';

export function StartTimeField() {
  const [time, setTime] = React.useState<string>();
  return (
    <Field>
      <FieldLabel htmlFor="start">Start time</FieldLabel>
      <TimePicker id="start" value={time} onChange={setTime} />
    </Field>
  );
}

The value and the list

value is a time on a 24-hour clock as "HH:mm", like "13:30", which is what an <input type="time"> holds, and onChange receives the picked one in the same form. Time picker keeps no value of its own, so set value from onChange. A value that is not a time shows the placeholder.

The list holds the whole day, step minutes apart, from 12:00 AM. step is 15 by default, so there are 96 times, and 30 or 60 keeps a long list short. A value that falls between steps, like 9:10 with a step of 15, is put in the list in its place, so the chosen time is always there to see. The list opens scrolled to the chosen time, or to 9:00 AM when there is none.

The button and the list read a time in hours and minutes with AM or PM, like "1:30 PM", in English whatever the page's language. The button is an outline Button at sm, 36px tall and full width, with a clock icon before the time, and placeholder in muted text while there is no value. variant="ghost" drops the border. id, disabled, and className go on the button.

With a date

Time picker has no date. When a moment needs both, put a Date picker beside it and join the two values where you save them. A wider date and a narrower time read as one moment.

<div className="grid grid-cols-[3fr_2fr] gap-3">
  <Field>
    <FieldLabel htmlFor="send-on">Send on</FieldLabel>
    <DatePicker id="send-on" value={date} onChange={setDate} />
  </Field>
  <Field>
    <FieldLabel htmlFor="send-at">Time</FieldLabel>
    <TimePicker id="send-at" value={time} onChange={setTime} step={30} />
  </Field>
</div>

Props

Time picker takes these eight props and nothing else.

PropTypeDefaultWhat it does
valuestringThe chosen time as "HH:mm". Leave it undefined for none.
onChange(time: string) => voidCalled with the picked time as "HH:mm".
stepnumber15Minutes between the times in the list.
placeholderstring"Pick a time"Shown in muted text while there is no value.
idstringGoes on the button, for a FieldLabel's htmlFor.
disabledbooleanDisables the button, so the list cannot open.
classNamestringClasses for the button.
variant"outline" | "ghost""outline"The button's variant. ghost has no border.

formatTime comes with it and reads an "HH:mm" value the way the button does, for showing a chosen time somewhere else, like a summary.

Accessibility

  • The button has aria-haspopup="dialog" and aria-expanded. Point a FieldLabel's htmlFor at its id, and the label names it.
  • Opening moves focus to the list, a listbox named "Times". The time you are on is its active option, which a screen reader announces as you move, and the chosen time is selected and has a check.
  • Up and Down move one time, Page Up and Page Down move four, and Home and End go to the first and last. Enter or Space picks the time and closes the popover.
  • Typing a time jumps to it. "9" goes to 9:00, "930" to 9:30, and "21" or a "p" after it, like "9p", to the evening. Typing "9" again goes on to the next 9 o'clock.
  • Escape closes the popover and returns focus to the button, and so does picking a time. A click outside closes it and leaves focus where you clicked.
  • The popover is a dialog with no name, and it is not modal.
  • There is no field to type a time into, so the list is the only way to enter one.
  • The button is 36px tall, and the button and each time are at least 44px on a touch screen.
  • In forced colors the time you are on shows in the system highlight.
  • Under reduced motion the popover opens and closes without its fade.

When to use something else

  • Select for a few named times, like Morning or Evening.
  • Input with type="time" when people know the exact minute and type faster than they scroll, like logging hours.
  • Date picker for the day, beside this for the time.

Install

shadcn add @gesso/time-picker

It brings Popover and Button.

On this page