DateRangePicker
Usage
Inspection window
Committed range: 2026-09-10 to 2026-09-20
const [range, setRange] = useState<DateRangeValue | null>({
start: "2026-09-10",
end: "2026-09-20",
});
const [rangeValid, setRangeValid] = useState(true);
<DateRangePicker
id="inspection-window"
label="Inspection window"
value={range}
onValueChange={setRange}
onDraftValidityChange={setRangeValid}
min="2026-01-01"
max="2026-12-31"
required
/>
<Button disabled={!rangeValid}>Review</Button>
Range values
DateRangeValue is { start: string; end: string }. Each endpoint is an exact Gregorian YYYY-MM-DD date in years 0001–9999, with no time or time zone. The pair may be null. Start must be on or before end; equal-day ranges are valid. The inclusive min and max bounds apply to both endpoints.
The start and end fields each have their own calendar popup. Choose an endpoint from its calendar or edit its segments. The component publishes only complete ordered pairs, or null when both fields are empty. Reversed or partial drafts remain visible and invalid.
Connect onDraftValidityChange to Review and Save. It stays false while either endpoint is incomplete, the pair is invalid, or the host has not accepted a complete changed pair. The previous public range must not be submitted while a local endpoint is unfinished.
An externally changed range replaces both endpoint drafts. To discard local editing while retaining the same pair, change the component's React key.
Labels, focus, and accessibility
Supply a group label; use startLabel and endLabel when the default labels need more context. The id and forwarded HTMLDivElement ref identify the focusable group. Focusing it enters the first start-date segment for error-summary navigation. React Aria supplies each endpoint's segment and calendar keyboard behavior, and closing a calendar, with Escape or by choosing a day, returns focus to that endpoint's calendar button.
Invalid ranges show readable errors. A required range marks both endpoints' segments required for assistive technology and says once that the range is missing. disabled and readOnly prevent changes to both endpoints. The start and end fields sit side by side when the picker itself is wide enough for both, and stack into one column when it is not, whatever the window width; they keep their segment focus indicators and 24 CSS pixel minimum targets.
The callback receives (valid, { pending }). Use valid to gate Review/Save and pending to track unfinished local editing for leave protection or draft persistence. A pristine required-empty field is invalid but has pending: false; partial input has pending: true. Malformed supplied values also remain clean until edited. Existing callbacks that accept only valid continue to work.
Props
| PROP | TYPE | DEFAULT | REQUIRED |
|---|---|---|---|
label | string | — | yes |
value{ start: YYYY-MM-DD, end: YYYY-MM-DD }, or null. Dates carry no time or time zone. | node | — | yes |
onValueChangeRequests complete ordered ranges or null when both endpoints are cleared. Partial and reversed ranges are retained as invalid drafts. | function | — | yes |
onDraftValidityChangeCallback (valid, { pending }). Pending identifies uncommitted local edits; pristine required-empty or malformed supplied values are invalid but not pending. False for incomplete, invalid, out-of-bounds, required-empty, or unaccepted range drafts. Block Review/Save while false. | function | — | no |
startLabel | string | "Start date" | no |
endLabel | string | "End date" | no |
minInclusive YYYY-MM-DD lower bound for both endpoints. | string | — | no |
maxInclusive YYYY-MM-DD upper bound for both endpoints. | string | — | no |
disabled | boolean | false | no |
readOnly | boolean | false | no |
required | boolean | false | no |
idID of the focusable group. Focusing it or its forwarded HTMLDivElement ref enters the first start-date segment. | string | — | no |
className | string | — | no |
Manifest entry
<DateRangePicker id="inspection-window" label="Inspection window" value={range} onValueChange={setRange} onDraftValidityChange={setRangeValid} required />View agent manifest
{
"description": "Controlled ordered Gregorian date range with independently editable start and end fields and separate endpoint calendars. Equal-day ranges are valid.",
"props": {
"label": {
"type": "string",
"required": true
},
"value": {
"type": "node",
"required": true,
"description": "{ start: YYYY-MM-DD, end: YYYY-MM-DD }, or null. Dates carry no time or time zone."
},
"onValueChange": {
"type": "function",
"required": true,
"description": "Requests complete ordered ranges or null when both endpoints are cleared. Partial and reversed ranges are retained as invalid drafts."
},
"onDraftValidityChange": {
"type": "function",
"description": "Callback (valid, { pending }). Pending identifies uncommitted local edits; pristine required-empty or malformed supplied values are invalid but not pending. False for incomplete, invalid, out-of-bounds, required-empty, or unaccepted range drafts. Block Review/Save while false."
},
"startLabel": {
"type": "string",
"default": "Start date"
},
"endLabel": {
"type": "string",
"default": "End date"
},
"min": {
"type": "string",
"description": "Inclusive YYYY-MM-DD lower bound for both endpoints."
},
"max": {
"type": "string",
"description": "Inclusive YYYY-MM-DD upper bound for both endpoints."
},
"disabled": {
"type": "boolean",
"default": false
},
"readOnly": {
"type": "boolean",
"default": false
},
"required": {
"type": "boolean",
"default": false
},
"id": {
"type": "string",
"description": "ID of the focusable group. Focusing it or its forwarded HTMLDivElement ref enters the first start-date segment."
},
"className": {
"type": "string"
}
},
"composable_with": [
"FormSection",
"FormErrorSummary",
"DatePicker"
],
"never_inside": [
"Button"
],
"example": "<DateRangePicker id=\"inspection-window\" label=\"Inspection window\" value={range} onValueChange={setRange} onDraftValidityChange={setRangeValid} required />"
}