DatePicker

A date field with a calendar in a Popover — type the date or pick it from a month grid navigated with arrow keys — for one date or a start–end range, with values as plain ISO dates so no timezone ever shifts a birthday.

Basic usage

Install the package:

pnpm add @design-schema/react @design-schema/tokens

Import the stylesheet once for the app, then the component:

import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { DatePicker } from '@design-schema/react';

Examples

import { DatePicker } from '@design-schema/react';

<DatePicker label="Due date" name="due" />

Props

18 rows
PropTypeDefaultDescriptionPlatforms
label requiredstring—

Visible label ("Start date", "Date of birth").

Accessibility: label/for on the input; the grid is named "{label}, {month} {year}".

web, lit, rn, swiftui
name requiredstring—

Field name for the Form. The value is an ISO calendar date string (`2026-09-10`) or, for a range, `{ start, end }` of them in `value` and `onChange`. The Form holds strings only, so a range registers two fields, `name` (start) and `name-end` (end). Never a Date object: a calendar date has no time zone.

web, lit, rn, swiftui
valuestring | { start: string; end: string }—

Controlled value (ISO date, or a range). Pass `''` for a controlled empty field (`{ start: '', end: '' }` for a range; an empty start or end is a missing end); `undefined` means uncontrolled. While an input has focus it keeps the typed text; on blur, and at once after a pick or Clear, the inputs show the formatted `value`, so a controlled owner that does not update `value` sees the text revert. In a range the first pick is an internal draft shown only in the calendar: `value` and the inputs keep showing the old value until the end is picked, then onChange reports the range and the field returns to `value`. Typing does not touch a pending draft — only a second pick, Clear, or closing the calendar resolves or discards it — so a user may type an end date while a draft start shows in the calendar, and the two do not interact.

web, lit, rn, swiftui
defaultValuestring | { start: string; end: string }—

Initial value.

web, lit, rn, swiftui
openboolean—

Controlled calendar state, for programmatic use and for stories and tests. Omit for the button-driven default. Every change to open, by the user or by the parent, aims the calendar at the value's month (or today's) and focuses the selected day (or today). "The value" is the committed value only — its end when opened by ArrowDown in the end input, its start otherwise (the calendar button always opens on the start) — and uncommitted typed text is ignored. Lit: a property only, not a reflected attribute, because an absent attribute cannot say controlled-closed; bind `.open`, and setting it to false keeps it controlled. Focus returns to the calendar button on the state change, not on the request: a controlled owner that ignores `onOpenChange` keeps the calendar open and focus stays inside it.

web, lit, rn, swiftui
rangebooleanfalse

Pick a start and an end date in one calendar; two inputs in the field.

web, lit, rn, swiftui
minstring—

Earliest selectable date (ISO). Earlier days are disabled and the error uses `copy.tooEarly`. In a range, either end before `min` reports tooEarly (and either end after `max` tooLate), checked only once the range is complete.

web, lit, rn, swiftui
maxstring—

Latest selectable date (ISO).

web, lit, rn, swiftui
isDateDisabledfunction (isoDate: string) => boolean—

Disable specific days (weekends, holidays, booked). Disabled days are shown, not hidden, and are skipped by the Arrow keys (moving on in the same direction to the next enabled day, turning pages, and staying put when none is left before min/max, or, with no bound in that direction, when none is found within 3660 days, about ten years); Home, End, PageUp and PageDown land on the computed day even when it is disabled. A disabled day can take focus but not be selected. The skipping rule, and the 3660-day limit with it, is web and Lit only: React Native has no arrow navigation, so there is nothing there to skip with.

web, lit, rn, swiftui
localestring—

BCP 47 locale for month and weekday names, the first day of the week, and the typed format. Defaults to `document.documentElement.lang` when set (web, lit), then the `Intl.DateTimeFormat().resolvedOptions().locale` default (the device locale on rn). The typed pattern comes from `formatToParts` with 2-digit month and day and a numeric year, so en-US is MM/DD/YYYY.

web, lit, rn, swiftui
showWeekNumbersbooleanfalse

An ISO week-number column at the start of each row.

web, lit, rn, swiftui
placeholderstring—

Defaults to the locale's pattern ("MM/DD/YYYY", "DD.MM.YYYY").

web, lit, rn, swiftui
descriptionstring—

Helper text.

web, lit, rn, swiftui
requiredbooleanfalse

Must have a value to submit. `copy.requiredIndicator` is appended to the visible label text, so it is part of the input's accessible name, as Input.

web, lit, rn, swiftui
hideLabelbooleanfalse

Visually hide the label (it remains the accessible name). Only for a field whose context already names it: a DataGrid cell editor, a Search. Hidden with the visually-hidden clip pattern, so it needs no binding. React Native has no such pattern: the label Text is not rendered and the input's accessibilityLabel carries the name, as Input.

web, lit, rn, swiftui
sizesm | mdmd

sm for fields inside grid cells and toolbars: minimum target height, tighter padding, small type (`fontSize` at font.size.sm).

web, lit, rn, swiftui
disabledbooleanfalse

Not editable, still readable. Every user-driven open is blocked — the calendar Button, ArrowDown in the input — but a controlled `open: true` still renders the calendar: a controlled prop is the parent's decision, not a user interaction.

web, lit, rn, swiftui
errorstring—

Error message; implies invalid.

web, lit, rn, swiftui

Events

2 rows
EventTypeDescriptionPlatforms
onChangeonChange / change

Fired when a complete valid date (or range) is typed or picked, with the ISO value; with undefined when cleared. Every pick fires, including re-picking the day already selected; typing text that parses to the current value does not. Clear fires undefined even when the field is already empty.

web, lit, rn, swiftui
onOpenChangeonOpenChange / open-change

Fired when the calendar opens or closes.

web, lit, rn, swiftui

Accessibility

Decisions this component leaves to you

Supply the label. It is associated with the control for you, but the words are yours — a placeholder is not a label.

Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.

Supply the error text when the value is invalid. The component links and announces it; it cannot write it.

Required props that carry accessibility meaning: label. Each is described in the props table above.

No implicit ARIA role — the component is styling and structure, and adds nothing to the accessibility tree.

Anatomy

labeldescriptionfieldinputcalendarButtonpopoverheaderprevMonthButtonnextMonthButtonmonthSelectyearSelectgridweekdayHeaderweekNumberdayfootertodayButtonclearButtonerrorMessage

What generation guarantees

Label association — The label is programmatically associated with the control, not merely placed beside it.

Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.

Error identification — An invalid value is identified in text and linked to the control — never signalled by colour alone.

Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.

Arrow navigation — Arrow keys move between items; Home and End jump to the ends.

Roving tabindex — The group is a single tab stop; the arrow keys move within it.

Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.

Focus restore — On close, focus returns to whatever opened it.

Escape dismisses — Escape closes it from anywhere inside.

Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.

Target size — Interactive targets are at least 44×44 CSS pixels — the touch-first size, not the minimum.

Selected state — Selection is exposed programmatically (`aria-selected` / `aria-checked`).

Reduced motion — Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.

DatePicker keyboard support

12 rows
KeyAction
ArrowDown or Alt+ArrowDownFrom the input, opens the calendar with focus on the selected day (or today); when the calendar is already open, moves focus to that day (the pending range start, else the value, else today) — from the end input it aims at the end date, so ArrowDown and reopening behave the same way from the same input. (focus in input)
Enter or SpaceOn the calendar button, opens; on a day, selects it (and closes for a single date; for a range, selects the start then the end).
EscapeCloses the calendar without changing the value. Focus returns to the calendar button when it was inside the calendar; when it is in an input or on the button itself it stays there, since Escape only takes back focus the calendar took. The handler is DatePicker's own, on its root: the non-modal Popover cannot hear a key pressed in the field, which is outside its panel. (open)
ArrowRightNext day. (focus on a day)
ArrowLeftPrevious day. (focus on a day)
ArrowDownSame weekday, next week. (focus on a day)
ArrowUpSame weekday, previous week. (focus on a day)
HomeFirst day of the week. (focus on a day)
EndLast day of the week. (focus on a day)
PageUpSame day, previous month (Shift: previous year). (focus on a day)
PageDownSame day, next month (Shift: next year). (focus on a day)
TabCycles within the calendar: previous month, month Select, year Select, next month, the grid (one tab stop, roving over days), Today, Clear, and back (Shift+Tab reverses). DatePicker traps Tab itself, because the non-modal Popover would close on Tab-out; Tab inside an open Select popup belongs to the Select. In the grid, keys held with Alt, Ctrl or Meta are ignored. (open)

Contrast pairs

9 rows
ForegroundBackgroundLevel
color.foregroundcolor.backgroundAA
color.foreground.mutedcolor.backgroundAA
color.foreground.dangercolor.backgroundAA
color.foregroundcolor.overlay.surfaceAA
color.foreground.mutedcolor.overlay.surfaceAA
color.foregroundcolor.background.strongAA
color.control.selectedForegroundcolor.control.selectedBackgroundAA
color.control.selectedBackgroundcolor.overlay.surfaceAA (non-text)
color.border.strongcolor.backgroundAA (non-text)

Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.