Date Field

Segmented date 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.

Departure date
mmddyyyy

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="date"> 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.

Locale reorders the field

locale decides the segment order, not just the labels.

en-US renders month / day / year; en-GB renders day / month / year. Passing the wrong locale does not merely relabel the field — it rearranges it, and a user typing their own date format into the wrong order silently enters the wrong date. That is a correctness bug, not a preference, and it is asserted in the tests.

Granularity

granularity decides which segments exist at all. day (the default) gives a plain date; minute adds hour and minute segments, plus AM/PM in a 12-hour locale. Ask for the smallest unit you actually need — every extra segment is another thing the user has to fill.

Props

DateField.Root — value and placeholder are bindable.

PropTypeDefault
value The date value. Bindable. A `DateValue`, not a JS `Date`.DateValue | undefined—
onValueChange Called when the value changes.(value: FieldValue) => void—
placeholder Which date the empty segments are formatted from. Bindable.DateValue—
minValue Earliest accepted value.DateValue—
maxValue Latest accepted value.DateValue—
granularity Smallest unit the user can edit, which decides which segments appear. `day` gives a plain date; `minute` adds time segments.'day' | 'hour' | 'minute' | 'second''day'
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 Marks the field as required. There is no single element to mark: the Root is a wrapper, and what a screen reader lands on are the segments. So this reaches them through context and becomes `aria-required` on each editable one, whose `role="spinbutton"` supports it (checked against axe, not assumed). Bits emits nothing for `required` on this component, so before it was wired up a `<Field required>` around a date field announced nothing at all. It is still forwarded to Bits, which uses it on the hidden input for form submission.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.