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
| Name | Type | Default | Description |
|---|---|---|---|
value | string | null | The chosen time as "HH:mm" (24 h, like a native time input) – makes it controlled; null shows the placeholder | |
defaultValue | string | null | null | Starting time when uncontrolled |
onChange | (value: string | null) => void | Called with "HH:mm" when a time is picked or typed, or null when the field is cleared | |
step | number | 15 | Minutes between the times in the list, counted from min – typing can still set any minute |
min | string | The earliest time, "HH:mm" – the list starts here, earlier typed times are put back | |
max | string | The latest time, "HH:mm" – ignored when it lies before min | |
hourCycle | 12 | 24 | 9:30 PM or 21:30 – by default the locale's | |
locale | string | BCP 47 locale for how times are written – the browser's by default; its own AM / PM words can be typed too | |
placeholder | string | Shown in gray while nothing is chosen – by default 9:00 in the right format, as an example | |
label | ReactNode | Shown above the field and used as the list's name | |
hint | ReactNode | A quiet line under the field – the error takes its place while there is one | |
error | ReactNode | Marks the field invalid (red edge and a faint red fill, aria-invalid) and shows the message where the hint was | |
optional | boolean | false | Adds "Optional" next to the label |
size | "sm" | "md" | "lg" | "md" | 32 · 36 · 40px high, like the Text Field; taller on touch screens |
disabled | boolean | false | Dims the field and ignores input |
name | string | Sends "HH:mm" with a form, through a hidden input | |
required | boolean | Sets aria-required – check it on submit like any other field | |
aria-label | string | Accessible name when there is no visible label | |
className | string | Extra class on the wrapper, e.g. a fixed width in a settings row | |
ref | Ref<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