Skip to content

Time picker

Picks a time of day, typed in the field or chosen from a list of times. Holds a time with no date, like a Time field.

Installation

pnpm dlx shadcn@latest add @wisty/time-picker

The CLI installs the Radix or Base UI file to match your project, with what it builds on: combobox, input-group.

If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.

When to use

Use it

  • People enter a time of day in a form, like an event's start time or a preferred call time.
  • People type the time or pick it from a list of times under the field, every 15 minutes by default. The field shows it in the locale's style, "9:30 PM" in en-US and "21:30" in de-DE.
  • The value is a time with no date and no time zone, like a Salesforce Time field. The time picker holds it as "HH:MM", or "HH:MM:SS" with seconds.
  • Only some times are allowed, like business hours. Pass min and max.
  • It replaces lightning-input with type="time".

Don't use it

  • The value is a date and a time, like a Salesforce Date/Time field. Use Datetime picker.
  • The value is a day. Use Date picker.
  • People only type the time and don't need a list. Use Input with type="time", which also gives phones their own time wheel.
  • The time is a duration, like a call's length in minutes. Use Number input.
  • The time is only read. Show it as text.

Usage

import {
  TimePicker,
  TimePickerContent,
  TimePickerInput,
  TimePickerList,
} from "@/components/wisty/time-picker"
TimePicker
├── TimePickerInput
└── TimePickerContent
    └── TimePickerList
        └── TimePickerItem

TimePicker holds the value, the typed text and the list's state; the other parts only draw. TimePickerList renders a TimePickerItem for each time on its own, so most fields stop at the list. onValueChange gets the time, or null with the reason when the text isn't a time the field accepts:

"use client"

import { useState } from "react"

import { type TimePickerError } from "@/components/wisty/time-picker"
export function StartTime() {
  const [startTime, setStartTime] = useState<string | null>(null)
  const [error, setError] = useState<TimePickerError>()

  return (
    <TimePicker
      name="StartTime"
      value={startTime}
      onValueChange={(value, details) => {
        setStartTime(value)
        setError(details.error)
      }}
    >
      <TimePickerInput aria-label="Start time" />
      <TimePickerContent>
        <TimePickerList />
      </TimePickerContent>
    </TimePicker>
  )
}

error is "format" for text it can't read, and "min" or "max" for a time outside the bounds. In a form, name submits the time as "HH:MM", 24-hour. The field reads the locale's style, 24-hour times ("21:30"), AM or PM in a word or a letter ("9:30p", "9pm"), an hour alone ("9") and digits with no separator ("930", "2130"). To accept other text, like "noon", pass your own parse.

Examples

Step and bounds

Business hours only: the list runs from `min` to `max` every 30 minutes with `step`, and a typed time outside them is an error.

Business hours, 8:00 AM to 6:00 PM.

Seconds

A time with seconds, shown, typed and reported as "HH:MM:SS" with `timeStyle="medium"`.

Locale

German on the 24-hour clock, from `locale`: the field and the list show 14:30, and typing reads it.

States

A required field showing the error `onValueChange` reports, a `disabled` one and a `readOnly` one holding a time.

In a dialog

A call time in a modal dialog: the list opens over it, and Escape closes the list first, then the dialog.

API reference

PartBuilt on
TimePicker
TimePickerInputComboboxInput
TimePickerContentComboboxContent
TimePickerListComboboxList
TimePickerItemComboboxItem

TimePicker

PropTypeDefaultDescription
valuestring | nullThe time of day as 24-hour "HH:MM", or null when the field is empty or its text isn't a time.
defaultValuestring | nullnullThe time at first, when uncontrolled.
onValueChange( value: string | null, details: TimePickerChangeDetails ) => voidCalled with the new time, or null and the reason the text isn't one.
minstringThe earliest time people can pick or type, as "HH:MM" or "HH:MM:SS"; the list starts here.
maxstringThe latest time people can pick or type, as "HH:MM" or "HH:MM:SS".
stepnumber15Minutes between the times in the list. Typed times aren't rounded to it.
localestring"en-US"Formats and reads times, and sets the 12- or 24-hour clock.
timeStyleTimePickerStyle"short""medium" shows seconds, and takes and reports "HH:MM:SS".
parseTimePickerParseparseTimeReads typed text into "HH:MM" or "HH:MM:SS", or null when it isn't a time.
namestringSubmits the value with a form, from a hidden input.
requiredbooleanfalseRequired with a form.
disabledbooleanfalseDisables the field and its list.
readOnlybooleanfalseShows the time without the list; it can't be changed.
childrenReact.ReactNodeThe input and the list.

TimePickerItem

PropTypeDefaultDescription
value*stringThe time, as "HH:MM" or "HH:MM:SS".

parseTimefunction

Reads a typed time, in the locale's style or not: "9:30p" and "2130" are "21:30", "9" is "09:00". Returns "HH:MM:SS" with seconds, and null when the text isn't a time.

ParameterTypeDefault
text*string
__1*{ locale: string; seconds: boolean }

Returns string | null.

TimePickerErrortype

type TimePickerError = "format" | "min" | "max"

TimePickerChangeDetailstype

MemberTypeDefaultDescription
text*stringThe field's text when the value changed.
errorTimePickerErrorWhy the text isn't a time the field accepts. Unset when it is.

TimePickerParsetype

type TimePickerParse = (
  text: string,
  options: { locale: string; seconds: boolean }
) => string | null

TimePickerStyletype

type TimePickerStyle = "short" | "medium"

SLDS mapping

The time picker is SLDS's timepicker as lightning-input draws it: the autocomplete combobox with slds-timepicker on the form element. TimePickerInput is slds-combobox__input with the decorative clock icon on the right (slds-input__icon_right, utility:clock). TimePickerContent is the fluid dropdown under the field (slds-dropdown slds-dropdown_fluid), TimePickerList its slds-listbox, and TimePickerItem a plain option (slds-listbox__option_plain) with the check on the selected time (slds-is-selected).

Known gaps:

  • SLDS swaps in the phone's own time input on mobile. The time picker keeps its field and list.
  • The clock icon is lucide's, where SLDS draws utility:clock.
  • The field's error text ("Enter a valid time.", "Value must be … or later.") is English. Times, AM and PM follow locale.
PropValueSLDSNotes
(no value)default (added)slds-timepicker, slds-combobox__input, slds-input__icon_right (utility:clock), slds-dropdown_fluid, slds-listbox__option_plainA typed field with the clock on the right, and a list of times under it with a check on the selected one.
step(minutes) (added)lightning-input stepMinutes between the listed times. 15 by default. Typed times aren't rounded to it.
timeStyleshort, medium (added)lightning-input time-styleHow times show: 9:30 PM (the default) or 9:30:00 PM, in en-US. Medium takes and reports seconds.
locale(a locale) (added)lightning-input's user localeThe 12- or 24-hour clock, the words for AM and PM, and the digits. Defaults to en-US.
min, max(a time) (added)slds-has-errorThe list runs from min to max, and a typed time outside them is an error.
aria-invalidtrue (added)slds-has-error, slds-icon-text-errorSet by the time picker for text it can't read or a time outside the bounds, or by the caller.
disabledtrue (added)slds-input[disabled]
readOnlytrue (added)slds-input[readonly]The time as text, without the field's frame or the clock.

Keyboard interactions

Keys Action
Typing Narrows the list to the times that start with the text, and opens it when one does.
Down Arrow, Up Arrow Open the list on the picked time, or the first, and move through the times.
Enter Picks the highlighted time. With none highlighted, reads the typed text, or with nothing typed, opens or closes the list.
Escape Closes the list and keeps the field's text. Inside a modal dialog, the first Escape closes only the list, and the next closes the dialog.
Tab Reads the typed text and moves on. Leaving the field does the same.

A click in the field opens the list on the picked time, and focus stays in the field to type.

Accessibility

  • Name the field with a FieldLabel whose htmlFor is TimePickerInput's id, or pass aria-label to it.
  • For an error you decide, like an empty required field, set aria-invalid on TimePickerInput and point aria-describedby at the message. The time picker sets aria-invalid itself, and the input's validity, for text it can't read and times outside min and max.
  • While the list is open, the rest of the page is hidden from screen readers, the field's label included. The input keeps its name.
  • In Cosmos dark the error text is about 4.3:1, below WCAG AA's 4.5:1. SLDS's own stylesheet does the same.

Performance

  • The list renders every time in it: 96 at the default 15 minutes. A step of a minute makes 1,440; keep it at 5 or more, or narrow it with min and max.

Rules

  • Keep the value a "HH:MM" or "HH:MM:SS" string, 24-hour and zero-padded. A Date carries a day and a time zone that a time of day doesn't have.
  • A value that isn't a time, like "9:00", shows as it is with a "format" error. One outside min and max shows with its error. Neither is moved into range, submitted with a form, or reported through onValueChange.
  • Changing locale or timeStyle shows the value again in the new style. Text typed and not yet read is replaced.
  • Disabled, the time picker submits nothing with a form, as a disabled input doesn't.
  • Never pass asChild or render.