DatePicker
Usage
Committed date: 2026-01-31
const [date, setDate] = useState<string | null>("2026-01-31");
const [dateValid, setDateValid] = useState(true);
<DatePicker
id="inspection-date"
label="Inspection date"
value={date}
onValueChange={setDate}
onDraftValidityChange={setDateValid}
required
/>
<Button disabled={!dateValid}>Review</Button>
Date values and unfinished edits
The controlled value is a Gregorian YYYY-MM-DD string in years 0001–9999, or null. It carries no time or timezone. min and max are inclusive dates in the same format. Invalid supplied values or bounds produce readable errors.
Always connect onDraftValidityChange when a form can review or submit the value. It reports initial validity and changes to it. A partial, impossible, out-of-bounds, or required-empty draft reports false even when value still contains an earlier valid date. Keep Review and Save blocked until it becomes true.
An impossible date such as February 31 is never emitted as a different day. If the underlying segmented field restores its controlled display after rejecting an edit, the visible error and false validity remain until a date edit, a calendar selection, or an externally changed value resolves it. Moving focus with Tab does not clear the rejection. An optional, fully empty field is valid.
Focus and disabled controls
The id and forwarded HTMLDivElement ref identify the date-control group. Calling focus() on that group enters the first date segment, so FormErrorSummary can target the supplied ID. Escape, or choosing a day, closes the calendar and returns focus to the calendar button, however the calendar was opened. disabled and readOnly both prevent date changes and calendar opening.
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 |
valueYYYY-MM-DD in years 0001–9999, or null. Values contain no time or timezone. | string | — | yes |
onValueChangeRequests a complete valid date or null after clearing. Invalid or coerced dates are not emitted. | function | — | yes |
onDraftValidityChangeCallback (valid, { pending }). Pending identifies uncommitted local edits; pristine required-empty or malformed supplied values are invalid but not pending. Reports initial and changed validity. False for partial, impossible, out-of-bounds or required-empty edits. Block Review/Save while false, even if value still contains a previous valid date. | function | — | no |
minInclusive YYYY-MM-DD lower bound. | string | — | no |
maxInclusive YYYY-MM-DD upper bound. | string | — | no |
disabled | boolean | false | no |
readOnly | boolean | false | no |
required | boolean | false | no |
motion | enumauto | none | "auto" | no |
idID of the focusable date-control group. Focusing it enters the first segment; the forwarded HTMLDivElement ref has the same behavior. | string | — | no |
className | string | — | no |
Accessibility
- React Aria labels the editable segments and calendar dialog and provides keyboard date navigation.
- Errors remain visible as text and the segments expose invalid state. An edit outside
minormaxnames the bound it crossed, as a supplied value does: "Choose a date on or before 2026-12-31." - The calendar button is named "Choose date" followed by the field label. A required field's " *" is shown but not read; the segments say they are required.
- Segment and calendar targets meet the 24 CSS pixel minimum. Text is 11px or larger, with the shared focus indicator.
- The calendar uses the shared popover motion policy, including keyboard and reduced-motion handling.
Manifest entry
<DatePicker id="inspection-date" label="Inspection date" value={date} onValueChange={setDate} onDraftValidityChange={setDateValid} required />View agent manifest
{
"description": "Controlled Gregorian date-only picker with segmented editing, calendar overlay and a draft-validity signal for incomplete or rejected edits.",
"props": {
"label": {
"type": "string",
"required": true
},
"value": {
"type": "string",
"required": true,
"description": "YYYY-MM-DD in years 0001–9999, or null. Values contain no time or timezone."
},
"onValueChange": {
"type": "function",
"required": true,
"description": "Requests a complete valid date or null after clearing. Invalid or coerced dates are not emitted."
},
"onDraftValidityChange": {
"type": "function",
"description": "Callback (valid, { pending }). Pending identifies uncommitted local edits; pristine required-empty or malformed supplied values are invalid but not pending. Reports initial and changed validity. False for partial, impossible, out-of-bounds or required-empty edits. Block Review/Save while false, even if value still contains a previous valid date."
},
"min": {
"type": "string",
"description": "Inclusive YYYY-MM-DD lower bound."
},
"max": {
"type": "string",
"description": "Inclusive YYYY-MM-DD upper bound."
},
"disabled": {
"type": "boolean",
"default": false
},
"readOnly": {
"type": "boolean",
"default": false
},
"required": {
"type": "boolean",
"default": false
},
"motion": {
"type": "enum",
"values": [
"auto",
"none"
],
"default": "auto"
},
"id": {
"type": "string",
"description": "ID of the focusable date-control group. Focusing it enters the first segment; the forwarded HTMLDivElement ref has the same behavior."
},
"className": {
"type": "string"
}
},
"composable_with": [
"FormSection",
"FormErrorSummary"
],
"never_inside": [
"Button"
],
"example": "<DatePicker id=\"inspection-date\" label=\"Inspection date\" value={date} onValueChange={setDate} onDraftValidityChange={setDateValid} required />"
}