A time of day with typed input and a popup picker; the range variant pairs start and end on one row.
TimeField lets people type a time the way they say it ("3pm", "0930", "14:30") and offers a popup picker as a second path: scrollable hour, minute and AM/PM columns that auto-center on the current value. The value is "HH:MM" 24h regardless of display format, so it sorts, compares and serialises predictably.
format="12h" shows "9:30 AM" and parses am/pm; the default is 24h. step sets the minute granularity for the popup list (default 15; set to 1 for a free picker). min and max bound both the parse and the picker — a chosen time outside the bounds is kept on screen with a reason, not silently wiped. TimeRangeField pairs two TimeFields with an en dash between them and flags end at or before start as invalid so the row disagrees with itself until the parent fixes it.
Installation
npx shadcn@latest add KezLahd/rime/time-fieldAlso installs: base, field, icon, text-input.
Usage
import { TimeField, TimeRangeField, type Time, type TimeRange } from "@/components/ui";
<TimeField value={at} onChange={setAt} />Examples
Open
Single time and range
API
Generated from the component's TypeScript types and JSDoc (scripts/gen-ui-registry.mjs), so it always matches the source.
TimeField
| Prop | Type | Default | Description |
|---|---|---|---|
| valueRequired | Time | null | ||
| onChangeRequired | (value: Time | null) => void | null when cleared or when the typed text can't be read as a time. | |
| format | "12h" | "24h" | "24h" | 24h is the default. 12h shows "9:30 AM" and parses am/pm. |
| step | number | 15 | Minute granularity for the popup list (default 15). Set to 1 for a free picker. |
| min | Time | ||
| max | Time | ||
| invalid | boolean | ||
| disabled | boolean | ||
| required | boolean | ||
| id | string | ||
| name | string | ||
| placeholder | string | ||
| aria-label | string | ||
| className | string | ||
| inline | boolean | false | Keep the popup rendered open under the field (for docs). |
TimeRangeField
| Prop | Type | Default | Description |
|---|---|---|---|
| valueRequired | TimeRange | ||
| onChangeRequired | (value: TimeRange) => void | ||
| format | "12h" | "24h" | "24h" | |
| step | number | 15 | |
| requireEndAfterStart | boolean | true | When true, flag an end at or before start as invalid. Default true. |
| min | Time | ||
| max | Time | ||
| disabled | boolean | ||
| className | string | ||
| aria-label-start | string | ||
| aria-label-end | string |
Theme tokens
Per-component tokens are undeclared by default, so the default shown is the fallback the CSS uses. Set one at :root (or in theme.css) to restyle every instance. Live values are read from this page, so they follow the Rime Studio.
| Token | Default | Live value |
|---|---|---|
| --field-focus-border | rgba(var(--rgb-brand), 0.7) / var(--focus-color) | … |
| --field-focus-ring | var(--focus-halo) | … |
System tokens it reads
| Token | Default | Live value |
|---|---|---|
| --danger-ink | tokens.css | … |
| --dur | tokens.css | … |
| --ease | tokens.css | … |
| --ease-out | tokens.css | … |
| --focus-color | tokens.css | … |
| --focus-halo | tokens.css | … |
| --ink-body | tokens.css | … |
| --ink-brand | tokens.css | … |
| --ink-disabled | tokens.css | … |
| --ink-heading | tokens.css | … |
| --ink-muted | tokens.css | … |
| --line | tokens.css | … |
| --r-lg | tokens.css | … |
| --r-sm | tokens.css | … |
| --r-xs | tokens.css | … |
| --rgb-brand | tokens.css | … |
| --shadow-float | tokens.css | … |
| --surface-solid | tokens.css | … |
| --surface-sunken | tokens.css | … |
| --text-small | tokens.css | … |
| --wash-active | tokens.css | … |
| --wash-hover | tokens.css | … |
| --wash-subtle | tokens.css | … |
| --z-popover | tokens.css | … |
Accessibility
- The field accepts spoken-style input ("3pm", "9:30 am") and reports a reason when the text can't be read as a time, named by aria-describedby.
- The popup is a role="dialog" with hour, minute and (12h only) AM/PM listboxes; arrow keys move focus between cells and Enter picks.
- Alt+Down from the field opens the popup. Escape closes it and returns focus to the toggle, so a Modal containing the field doesn't close with it.
Do and don't
Do
- Use step={15} or step={30} for scheduling flows; most times a human picks are on the quarter or half hour.
- Pass min and max for opening hours or an event window so the picker only lists times that work.
Don't
- Don't use a free-text input for a time; the picker catches typos and ambiguous formats cheaply.
For AI agents
This page as markdown, rendered from the same registry entry. Point an agent at it (or add .md to this page's URL), or at /llms.txt for the index.
Open time-field.mdRelated: date-field, date-range-filter, date-time-field