affectUI › Components › Inputs

Time Picker

A time of day you type or pick – a list every few minutes with a gliding highlight, 12 or 24 hours by locale, min and max, and typing that understands 930, 9:30 pm or 21.30.

Free and open source (MIT). Install: npx shadcn@latest add https://www.affectui.com/r/time-picker.json · Import from @/components/ui/time-picker

When to use

  • A time of day in a form: when an agent runs, when the daily digest is sent, quiet hours, a reminder
  • Next to a Date Picker for a date and a time, or on its own in a settings row – same height, edge, label, hint and error as the Text Field

When not to use

  • A day – use the Date Picker; a period of days – use the Date Range Picker
  • A duration ("for 45 minutes") or a rhythm ("every 2 hours") – a number field or a Select reads better
  • Three or four fixed slots people choose between – use the Segmented Control or a Select

Usage

import { useState } from "react";
import { TimePicker } from "@/components/ui/time-picker";

export function RunAt() {
  const [time, setTime] = useState<string | null>("09:30");
  return (
    <TimePicker
      label="Run at"
      value={time}
      onChange={setTime}
      step={15}
      hint="Every weekday, in your time zone."
      name="runAt"
    />
  );
}

Props

NameTypeDefaultDescription
valuestring | nullThe chosen time as "HH:mm" (24 h, like a native time input) – makes it controlled; null shows the placeholder
defaultValuestring | nullnullStarting time when uncontrolled
onChange(value: string | null) => voidCalled with "HH:mm" when a time is picked or typed, or null when the field is cleared
stepnumber15Minutes between the times in the list, counted from min – typing can still set any minute
minstringThe earliest time, "HH:mm" – the list starts here, earlier typed times are put back
maxstringThe latest time, "HH:mm" – ignored when it lies before min
hourCycle12 | 249:30 PM or 21:30 – by default the locale's
localestringBCP 47 locale for how times are written – the browser's by default; its own AM / PM words can be typed too
placeholderstringShown in gray while nothing is chosen – by default 9:00 in the right format, as an example
labelReactNodeShown above the field and used as the list's name
hintReactNodeA quiet line under the field – the error takes its place while there is one
errorReactNodeMarks the field invalid (red edge and a faint red fill, aria-invalid) and shows the message where the hint was
optionalbooleanfalseAdds "Optional" next to the label
size"sm" | "md" | "lg""md"32 · 36 · 40px high, like the Text Field; taller on touch screens
disabledbooleanfalseDims the field and ignores input
namestringSends "HH:mm" with a form, through a hidden input
requiredbooleanSets aria-required – check it on submit like any other field
aria-labelstringAccessible name when there is no visible label
classNamestringExtra class on the wrapper, e.g. a fixed width in a settings row
refRef<HTMLInputElement>The text input – focus it after a validation error

Keyboard

  • ↓ / ↑: Open the list at the chosen time (or the next one from now); inside it, move between times
  • PageDown / PageUp: In the open list: an hour later or earlier
  • 0–9 / A–Z: Type a time – "9", "930", "9:30 pm", "21.30"; the list jumps to it as you type
  • Enter: Pick the highlighted time, or keep what was typed
  • Esc: Close the list; pressed again, undo what was typed
  • Tab: Keep what was typed and move on

Accessibility

  • The editable combobox pattern: a text input with role="combobox" and aria-autocomplete="list" that controls a listbox; the focus stays in the input while aria-activedescendant follows the highlight
  • Typed text that isn't a time, or lies outside min / max, is put back to the last good time when the field is left – nothing half-typed is ever sent
  • The label names both the field and the list; hint and error are linked with aria-describedby, an error sets aria-invalid
  • No focus ring on the text field – its edge firms up instead; the list button is reached by pointer only, the keys open the list from the input

Responsive

  • Fills its container – about 120px is enough; the list is as wide as the field
  • The list opens where there is room and stays inside the window; it lives in the top layer, so a card's overflow never cuts it
  • Touch screens get 40px rows and 16px text (no zoom on iOS); dark mode built in via .dark or data-theme="dark" on an ancestor

Requirements

  • React 19

Related

  • Date Picker – Pick one day in the Text Field's look – a month in a small card, today marked, days outside min and max or ruled out stay visible, full keyboard control and the locale's own week
  • Select – Pick one option in the Text Field's look – icons and a second line per option, a gliding highlight, type-ahead and a list that never gets cut off
  • Text Field – One input for every form – label, hint and error in place, prefix and suffix, clear, show-password and copy buttons, a character count
  • Date Range Picker – Presets and two calendars with a hover preview, comparison with the previous period or year, full keyboard control