Time Field

Segmented time input.

Usage

Compose Root > Label + Input, and render a Segment per part from the snippet Input hands you. Values are DateValue objects from @internationalized/date, a peerDependency.

Pickup time
––––AM

Why not a native input

This is not a text input with a mask. Every part is its own role="spinbutton" with aria-valuenow, aria-valuetext and its own aria-label — so arrow keys adjust it and a screen reader announces "month, 12" instead of reading a formatted string.

Typed numbers fill the focused segment and advance to the next, and the value is only committed once every segment is filled — so there is no half-parsed intermediate state to guard against in your code.

So the trade against <input type="time"> is: the same keyboard model, with markup and styling you control and identical rendering across browsers instead of whatever each vendor decided.

The separators are rendered too, as data-segment="literal" with aria-hidden — otherwise the field would announce "slash" between the parts. Key your each by index: segment parts repeat, so keying by part throws.

The 12/24 hour clock

Leave hourCycle unset and it follows the locale, which is almost always what you want — most of the world is on a 24-hour clock, so hardcoding 12 is a US-shaped assumption.

It also changes the structure: a 12-hour clock adds a dayPeriod (AM/PM) segment and bounds the hour at 1–12, while a 24-hour clock has no dayPeriod and bounds it at 0–23. Both are asserted in the tests.

Props

TimeField.Rootvalue and placeholder are bindable.

PropTypeDefault
value The time value. Bindable. A `TimeValue`, not a JS `Date`.TimeValue | undefined
onValueChange Called when the value changes.(value: FieldValue) => void
placeholder Which time the empty segments are formatted from. Bindable.Time | CalendarDateTime | ZonedDateTime
minValue Earliest accepted value.Time | CalendarDateTime | ZonedDateTime
maxValue Latest accepted value.Time | CalendarDateTime | ZonedDateTime
granularity Smallest unit the user can edit, which decides which segments appear.'hour' | 'minute' | 'second''minute'
hourCycle 12- or 24-hour clock. Leave it unset to follow the locale — hardcoding 12 is wrong for most of the world.12 | 24
locale BCP-47 locale. It decides the SEGMENT ORDER — month/day/year versus day/month/year — so getting it wrong does not just relabel the field, it rearranges it. Pass the user's locale.string'en'
name Name for form submission; Bits renders a hidden input.string
required booleanfalse
disabled booleanfalse
readonly booleanfalse
validate Return a message to mark the value invalid. Bits then sets `data-invalid` and wires `aria-invalid` on the segments.(value: never) => string | string[] | void
class Extra classes merged onto the root.string
children Snippet

Label, Input and Segment take class plus their native attributes. Segment needs the part from the snippet.