Date Range Field

Segmented date range input.

Usage

value is { start, end }, and you render two Inputs — one type="start" and one type="end". type is required; there is no single input that holds a range.

Put the visual separator between them yourself and mark it aria-hidden — a dash read aloud between two dates adds nothing.

Stay dates

You must name the Root

Bits gives the range Root role="group" with no accessible name. Without your help the field announces as an anonymous group.

Verified in the rendered DOM: Bits points each Input's aria-labelledby at your Label, but the Inputs carry no role — so nothing exposed to assistive technology ends up labelled. On the single-value Date Field the Input is the group and Bits does label it, so this gap is specific to ranges.

Pass aria-labelledby pointing at your Label's id, or aria-label. Both reach the root element, and both directions are asserted in the tests so this cannot regress quietly.

Shared with Date Field

Segment is the same component Date Field uses — Bits re-exports the identical module to both namespaces, so one styled implementation serves each and the two cannot drift apart. Everything in that page's "why not a native input" section applies here too.

Props

DateRangeField.Root — value and placeholder are bindable.

PropTypeDefault
value The date value range. Bindable. A `DateValue`, not a JS `Date`.{ start: DateValue | undefined; end: 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—

Input takes a required type of "start" or "end". Label and Segment take class plus their native attributes.