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| Prop | Type | Default | Description |
|---|---|---|---|
| label required | string | — | Visible label ("Start date", "Date of birth"). Accessibility: label/for on the input; the grid is named "{label}, {month} {year}". |
| name required | string | — | 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. |
| value | string | { 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. |
| defaultValue | string | { start: string; end: string } | — | Initial value. |
| open | boolean | — | 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. |
| range | boolean | false | Pick a start and an end date in one calendar; two inputs in the field. |
| min | string | — | 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. |
| max | string | — | Latest selectable date (ISO). |
| isDateDisabled | function (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. |
| locale | string | — | 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. |
| showWeekNumbers | boolean | false | An ISO week-number column at the start of each row. |
| placeholder | string | — | Defaults to the locale's pattern ("MM/DD/YYYY", "DD.MM.YYYY"). |
| description | string | — | Helper text. |
| required | boolean | false | 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. |
| hideLabel | boolean | false | 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. |
| size | sm | md | md | sm for fields inside grid cells and toolbars: minimum target height, tighter padding, small type (`fontSize` at font.size.sm). |
| disabled | boolean | false | 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. |
| error | string | — | Error message; implies invalid. |
Events
2 rows| Event | Type | Description |
|---|---|---|
| onChange | onChange / 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. |
| onOpenChange | onOpenChange / open-change | Fired when the calendar opens or closes. |
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
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.
Under `prefers-reduced-motion: reduce` the transitions are dropped rather than shortened, and the component still reaches every state.