artui
Components

Datepicker

View source

Accessible date input with calendar popup. Text input and calendar are both first-class entry paths.

npx artui@latest add datepicker

Playground

Selected: No date selected

The selected day uses the themeable --artui-accent color.

locale
min/max
isDateDisabled
required
error

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

PropTypeDefaultDescription
labelAccessibleTextnoneVisible 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-labelAccessibleTextnoneNon-visible accessible name. Use when a visible label is not desired. Mutually exclusive with label and aria-labelledby.
aria-labelledbystringnoneID of an existing element that labels this input. Mutually exclusive with label and aria-label.
valuerequiredDate | nullnoneThe currently selected date, or null when no date is selected.
onChangerequired(next: Date | null) => voidnoneCalled when the user selects a date via the calendar or types a valid date.
localestringnavigator.languageBCP-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.
minDatenoneEarliest selectable date (inclusive). Days before this are disabled.
maxDatenoneLatest selectable date (inclusive). Days after this are disabled.
isDateDisabled(date: Date) => booleannoneCustom predicate to disable individual dates.
errorstringnoneExternal validation error message displayed below the input and wired to the input via aria-describedby.
requiredbooleannoneMaps to aria-required on the text input.

Keyboard

Calendar popup follows the WAI-ARIA APG Date Picker Dialog pattern.

KeyAction
Arrow LeftPrevious day
Arrow RightNext day
Arrow UpPrevious week
Arrow DownNext week
HomeFirst day of the current month
EndLast day of the current month
Page UpPrevious month
Page DownNext month
EnterSelect focused day, close popup, return focus to trigger
SpaceSelect focused day, close popup, return focus to trigger
EscapeClose popup without selecting
TabCycle: prev-month → next-month → close button

Accessibility

CriterionNameHow the component satisfies it
1.3.1(A)Info and RelationshipsText 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)KeyboardDay 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 TrapFocus 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 OrderDOM 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 VisibleFocus 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 IdentificationInvalid 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 InstructionsThe 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, ValueEach 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 MessagesAn 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.

On this page