Field.TimePicker
Field.TimePicker is a button inside a Field that opens a list of times in a Popover. Its value is a Temporal.PlainTime or null, and onValueChange gets the time picked in the list, which then closes. The list has a time every step minutes from midnight, 30 by default, between min and max. It works like Field.DatePicker and takes the same props where they mean the same thing.
On this page
const id = useId();
const [time, setTime] = useState<Temporal.PlainTime | null>(null);
const [text, setText] = useState('');
const [invalid, setInvalid] = useState(false);
<Field.Group>
<Label.Root htmlFor={id}>Start time</Label.Root>
<Field.Root id={id} invalid={invalid}>
<Field.TextInput
value={text}
onChange={(event) => {
setText(event.target.value);
setTime(parseTime(event.target.value));
setInvalid(false);
}}
onBlur={() => {
if (time) setText(formatTime(time));
else setInvalid(text.trim() !== '');
}}
/>
<Field.TimePicker
value={time}
onValueChange={(next) => {
setTime(next);
setText(formatTime(next));
setInvalid(false);
}}
step={15}
showValue={false}
/>
</Field.Root>
</Field.Group>
Here a Field.TextInput in the same field lets people type the time too, including one between the list's times, such as 9:07. A picked time is written into the text with formatTime, and a typed time that parseTime reads shows in the list among its own when it is between min and max. Beside a Field.TextInput, that input is the field's control. It takes the field's id, name and description, and a press elsewhere in the field focuses it. With showValue={false}, the button shows only a clock glyph. The button sends the time with a form under its own name if it has one, and otherwise under the field's name, unless a Field part or a control joined with useField, other than a Field.ColorPicker, Field.DatePicker, Field.DateRangePicker, Field.TimePicker or Field.DateTimePicker, has that name, as a Field.TextInput does when it has no name of its own. To send the time as 09:30 rather than the typed text, put name on the picker instead of the Root.
Typed times
formatTime(time, { locale }) writes a time in the locale's short time style, such as 9:30 PM in en-US, 21:30 in de and 오후 9:30 in ko, or in its medium style, with seconds, when the time has seconds. parseTime(text, { locale }) reads typed text back into a Temporal.PlainTime, or returns null when the text is not a time. It reads an hour, an hour and minutes, or an hour, minutes and seconds, such as 9, 9:30, 21.30.15 or 0930, with the locale's own word for the part of the day before or after them, such as 9:30 pm in en-US or 오후 9:30 in ko. Letter case doesn't matter, digits may be in any script, and the numbers may be separated by any punctuation, symbols or spaces, or by words the locale writes between them, such as 21 h 30 in fr-CA. Without a word for the part of the day, the hour is 0 to 23. Without locale, both use the runtime's own.
In the example, text that isn't a time clears the time, so the form sends no time, and the field is marked invalid when it loses focus. parseTime doesn't check the picker's min or max, but a typed time outside them makes the form invalid.
On its own
On its own, Field.TimePicker is the field's control. It shows the time as formatTime writes it in locale, or placeholder while there is none, and a press elsewhere in the field opens the list. With a name on the Root, the form sends the time the way HTML writes one, such as 09:30, or 09:30:15 with seconds.
The list opens and closes through open with onOpenChange, or defaultOpen. onOpenChange gets the reason it closed: 'select' when a time is picked, 'escape', 'outside', 'trigger', or 'disabled' when the field becomes disabled. Open, the list takes focus and starts at the time, or else at the next time from now, or its first time. Escape closes it, and focus goes back to where it was before it opened.
required makes a form with no time invalid, and min and max make a form with an earlier or later time invalid, as well as limiting the list. A min later than max is an overnight range, as in a native time input: the list offers the times from min to midnight and from midnight to max, and the form accepts the same; a time in the gap between max and min is out of range, and the message names min, such as "Enter 10:00 PM or later". The browser checks all three on a native <input type="time"> that the picker renders beside its button. It is hidden from sight and from assistive technology, is not a tab stop, and is what the form sends under the field's name. Inside a Form.Root, a problem shows in the field's ErrorMessage.Root in the page's language, with min or max written as the list writes a time, and focus goes to the button rather than the hidden input; the summary's row for the field links to the button. Outside a Form.Root the browser blocks the submit and focus lands on the button too.
Temporal
Todae ships no Temporal polyfill. The picker's values, formatTime and parseTime are Temporal.PlainTime, so where the browser has no Temporal, the app loads a polyfill (see Native platform). Todae's types need TypeScript 6 or later, which declares Temporal.
Accessible composition
Todae handles: On its own, the button takes id, aria-label, aria-labelledby and aria-describedby from Field.Root, and native disabled and aria-invalid from the Root or a surrounding Fieldset. Its label names it, so the time it shows is added to its aria-describedby. Beside a Field part or a control joined with useField, other than a Field.ColorPicker, Field.DatePicker, Field.DateRangePicker, Field.DateTimePicker or another Field.TimePicker, such as a text input, that control takes the id and aria-describedby in its place, and the button keeps disabled and aria-invalid and is named from the timePicker string in StringsProvider unless it has its own aria-label. The button gets aria-haspopup="listbox" and aria-expanded, and aria-controls while the list is open. The list is an OptionList named from the timePicker string, in a popover with no role of its own. Arrow keys, Home and End move through the times, and Enter or Space picks one. The glyph is clock from GlyphsProvider. A required picker's button is described by the required string in StringsProvider, since a button cannot take aria-required.
You supply: A name, through a Label whose htmlFor is the Root id or aria-label / aria-labelledby on the Root. The same time for a text input and the picker beside it, written with formatTime and read with parseTime.
Watch for: A server render can't know another control sits beside the button, so until the page hydrates, the button and that control both carry the field's id, and both send a value under the field's name. With a value, the server writes that time in its own locale, so pass locale when the picker renders on a server.
When to use
Field.TimePicker
When: A time of day, such as opening hours, a daily reminder or when a meeting starts.
When not: A date and time in one value → Field.DateTimePicker. A duration → Field.NumberInput with an Affix for the unit.
formatTime
When: Writing a time as text for a locale, such as into a text input beside a picker.
When not: A time in another style, such as with a time zone → Intl.DateTimeFormat.
parseTime
When: Reading a time someone typed.
When not: Text in a fixed format such as 09:30 → Temporal.PlainTime.from.
Props
| Name | Type | Description |
|---|---|---|
value Required | Temporal.PlainTime | null | The time, or null for none. |
onValueChange | (value: Temporal.PlainTime) => void | Fires with the time picked in the list. |
min | Temporal.PlainTime | The earliest time the list offers. A form with an earlier time is invalid. Later than max, it makes an overnight range, from min through midnight to max. |
max | Temporal.PlainTime | The latest time the list offers. A form with a later time is invalid. |
required | boolean | A form with no time is invalid. |
step | number | Minutes between the times in the list, from midnight. Default: 30 |
locale | string | The locale for the times' text, such as "ko". The browser's own when not set, so pass it when the picker renders on a server. |
placeholder | string | Shown on the button while there is no time, when showValue is true. |
showValue | boolean | Whether the button shows the time, or the placeholder, beside its glyph. Set it to false beside a text input that shows the time. Default: true |
open | boolean | Controlled open state of the list. Pass with onOpenChange. |
defaultOpen | boolean | Initial open state when uncontrolled. |
onOpenChange | (open: boolean, reason: 'escape' | 'outside' | 'trigger' | 'select' | 'disabled') => void | Fires when the list opens or closes, with why it closed. |
| Name | Type | Description |
|---|---|---|
time Required | Temporal.PlainTime | The time to write. |
options.locale | string | The locale, such as ko. The runtime's own when not set. |
| Name | Type | Description |
|---|---|---|
text Required | string | The typed text. |
options.locale | string | The locale, such as ko. The runtime's own when not set. |