OPERATOR

DateRangePicker

Usage

Inspection window

Start date
End date

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

PROPTYPEDEFAULTREQUIRED
labelstring—yes
value

{ start: YYYY-MM-DD, end: YYYY-MM-DD }, or null. Dates carry no time or time zone.

node—yes
onValueChange

Requests complete ordered ranges or null when both endpoints are cleared. Partial and reversed ranges are retained as invalid drafts.

function—yes
onDraftValidityChange

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.

function—no
startLabelstring"Start date"no
endLabelstring"End date"no
min

Inclusive YYYY-MM-DD lower bound for both endpoints.

string—no
max

Inclusive YYYY-MM-DD upper bound for both endpoints.

string—no
disabledbooleanfalseno
readOnlybooleanfalseno
requiredbooleanfalseno
id

ID of the focusable group. Focusing it or its forwarded HTMLDivElement ref enters the first start-date segment.

string—no
classNamestring—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 />"
}

On this page