Datepicker
Accessible date input with calendar popup. Text input and calendar are both first-class entry paths.
npx artui@latest add datepickerPlayground
Selected: No date selected
The selected day uses the themeable --artui-accent color.
Usage
Controlled with visible label
import { useState } from 'react';
import { Datepicker } from '@artui/registry';
const [date, setDate] = useState<Date | null>(null);
<Datepicker label="Appointment date" value={date} onChange={setDate} />Every Datepicker needs exactly one of label, aria-label, or aria-labelledby.
Locale
<Datepicker
label="Datum"
value={date}
onChange={setDate}
locale="nl-BE"
/>locale accepts any BCP-47 tag and controls the input date format, the week start day, and the calendar month/year labels. Defaults to navigator.language. An invalid tag triggers a dev overlay and falls back to navigator.language.
Min and max
<Datepicker
label="Event date"
value={date}
onChange={setDate}
min={new Date(2026, 0, 1)}
max={new Date(2026, 11, 31)}
/>Days outside [min, max] render with aria-disabled="true" and the disabled attribute. They are not focusable and cannot be selected.
Disable specific dates
<Datepicker
label="Delivery date"
value={date}
onChange={setDate}
isDateDisabled={(d) => d.getDay() === 0 || d.getDay() === 6}
/>Returning true from isDateDisabled blocks that date.
External error
<Datepicker
label="Birth date"
value={date}
onChange={setDate}
error={serverError}
/>The error prop renders below the input and connects via aria-describedby.
Non-visible label
<Datepicker aria-label="Date of birth" value={date} onChange={setDate} />Use when a visible label would be redundant. Mutually exclusive with label and aria-labelledby.
API
| Prop | Type | Default | Description |
|---|---|---|---|
| label | AccessibleText | none | Visible label for the date input. Exactly one of label, aria-label, or aria-labelledby is required; passing none or two is a compile error. |
| aria-label | AccessibleText | none | Non-visible accessible name. Use when a visible label is not desired. Mutually exclusive with label and aria-labelledby. |
| aria-labelledby | string | none | ID of an existing element that labels this input. Mutually exclusive with label and aria-label. |
| valuerequired | Date | null | none | The currently selected date, or null when no date is selected. |
| onChangerequired | (next: Date | null) => void | none | Called when the user selects a date via the calendar or types a valid date. |
| locale | string | navigator.language | BCP-47 locale tag controlling date format and calendar week start. Defaults to the browser language. Passing an invalid BCP-47 tag triggers a dev overlay. |
| min | Date | none | Earliest selectable date (inclusive). Days before this are disabled. |
| max | Date | none | Latest selectable date (inclusive). Days after this are disabled. |
| isDateDisabled | (date: Date) => boolean | none | Custom predicate to disable individual dates. |
| error | string | none | External validation error message displayed below the input and wired to the input via aria-describedby. |
| required | boolean | none | Maps to aria-required on the text input. |
Keyboard
Calendar popup follows the WAI-ARIA APG Date Picker Dialog pattern.
| Key | Action |
|---|---|
| Arrow Left | Previous day |
| Arrow Right | Next day |
| Arrow Up | Previous week |
| Arrow Down | Next week |
| Home | First day of the current month |
| End | Last day of the current month |
| Page Up | Previous month |
| Page Down | Next month |
| Enter | Select focused day, close popup, return focus to trigger |
| Space | Select focused day, close popup, return focus to trigger |
| Escape | Close popup without selecting |
| Tab | Cycle: prev-month → next-month → close button |
Accessibility
| Criterion | Name | How the component satisfies it |
|---|---|---|
| 1.3.1(A) | Info and Relationships | Text input is a first-class entry path. Parse errors are wired to the input via aria-describedby. The calendar heading is a semantic <h2>. |
| 2.1.1(A) | Keyboard | Day cells are focusable role="gridcell" elements with a roving tabindex (APG Date Picker Dialog pattern); arrow keys are forwarded to the grid to move focus between days. Prev/next/close are native <button> elements. Tab cycles only among those three buttons inside the dialog. |
| 2.1.2(A) | No Keyboard Trap | Focus trap inside the dialog is limited to three buttons (prev, next, close). Escape always exits. Selecting a date also exits. |
| 2.4.3(A) | Focus Order | DOM order enforces logical focus sequence: prev-month → next-month → close button. Dialog is role="dialog" with aria-labelledby pointing at the month/year <h2>. |
| 2.4.7(AA) | Focus Visible | Focus ring uses outline (not box-shadow) on every interactive element including day cells, preserving visibility in Windows High Contrast mode. |
| 3.3.1(A) | Error Identification | Invalid typed dates produce an error message that is referenced by the input's aria-describedby. External errors from the error prop are handled identically. |
| 3.3.2(A) | Labels or Instructions | The label prop renders a visible <label> element. aria-label and aria-labelledby are alternate sources. Exactly one is required at compile time. |
| 4.1.2(A) | Name, Role, Value | Each day gridcell has an aria-label with the full date; the selected day has aria-selected="true", today has aria-current="date", and disabled days have aria-disabled="true". Nav buttons have aria-label with the target month name. aria-haspopup="dialog" and aria-expanded on the trigger. |
| 4.1.3(AA) | Status Messages | An aria-live="polite" aria-atomic region always present in the DOM announces the new month name when the user navigates between months. |
Do
Provide a label
<Datepicker label="Date of birth" value={date} onChange={setDate} />Use isDateDisabled for custom rules
<Datepicker
label="Delivery date"
value={date}
onChange={setDate}
isDateDisabled={(d) => isHoliday(d) || d.getDay() === 0}
/>Don't
Omit the label
<Datepicker value={date} onChange={setDate} /> {/* compile error */}Omitting all three label sources is a TypeScript compile error.
Combine label sources
<Datepicker label="Date" aria-label="Date" value={date} onChange={setDate} /> {/* compile error */}label, aria-label, and aria-labelledby are mutually exclusive.