DatePicker
Popover date grid using the ARIA grid pattern.
import { DatePicker } from '@elirobinson/react/components/organisms/DatePicker';Styles: @elirobinson/react/styles/organisms/DatePicker.css — already included when you import @elirobinson/react/styles.css.
Show code
import { useState } from 'react';
import { DatePicker } from '@elirobinson/react/components/organisms/DatePicker';
export default function Basic() {
const [value, setValue] = useState<Date | undefined>(new Date(2026, 7, 15));
return (
<DatePicker label="Session date" value={value} onValueChange={setValue} className="demo-col" />
);
}When to use it
Use DatePicker when someone is choosing a single specific day — a season start date, a
session, a deadline — and seeing the calendar grid actually helps (today, the weekday, how far
out it is). For a date range, or heavy typed-date entry like a birthdate years in the past, a
plain text input with format validation is often faster: this picker has no keyboard grid
navigation and no month/year jump, so getting to a far-off date means clicking "Next month"
repeatedly.
Starting empty
value is optional — leave it undefined for a field with nothing selected yet. There's no
defaultValue: the displayed value always comes straight from the value prop, so
onValueChange has to actually update your state for a click to visibly select a day.
Show code
import { useState } from 'react';
import { DatePicker } from '@elirobinson/react/components/organisms/DatePicker';
export default function Empty() {
const [value, setValue] = useState<Date | undefined>();
return (
<DatePicker
label="First practice"
value={value}
onValueChange={setValue}
className="demo-col"
/>
);
}Starting open
defaultOpen renders the calendar already open. It is uncontrolled — it seeds the initial
state and then stops mattering, so the field still toggles on click and still closes on
selection or an outside click. There is no controlled open prop to pair it with.
Reach for it when the calendar is the point of the step rather than one field among many — a booking flow whose only question is "which day?" — and when a screenshot or a design review needs to show the grid rather than the closed field. The popover is positioned absolutely, so it reserves no space: give the surrounding layout the height the open calendar needs, or it will paint over what follows.
One caveat if you prerender: today's date is read when the component renders, so a statically generated page bakes in whichever day the build ran and keeps showing it until the first client re-render. On a server-rendered open calendar, either mount it on the client or pick a month the marker cannot land in.
Show code
import { useState } from 'react';
import { DatePicker } from '@elirobinson/react/components/organisms/DatePicker';
/* The month is in the past deliberately. `today` is read at render, so a
prerendered page bakes the *build* machine's date into the grid and keeps it
until the first client re-render — a calendar opened on a month that cannot
contain a build date never shows a stale marker. */
const SEED_DATE = new Date(2026, 0, 8);
export default function Open() {
const [value, setValue] = useState<Date | undefined>(SEED_DATE);
return (
/* The popover is positioned absolutely, so it takes up no space of its own
and paints over whatever follows it. Reserve the room instead: a
six-week month is the tallest the calendar gets, at just over 300px
here, so the layout does not shift as you page through months. */
<div style={{ minHeight: 320 }}>
<DatePicker
label="Session date"
value={value}
onValueChange={setValue}
defaultOpen
className="demo-col"
/>
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | string | — | Accessible label for the trigger input (also its accessible name). |
onValueChangerequired | (date: Date) => void | — | — |
className | string | — | — |
defaultOpen | boolean | false | Renders with the calendar already open. Uncontrolled, like the same prop on Popover, DropdownMenu, Dialog and Sheet: it seeds the initial state and then stops mattering, so the picker still opens and closes on its own. There is no controlled `open` counterpart — after the first render the picker owns its own open state. |
value | Date | — | — |
Accessibility
- The calendar uses the WAI-ARIA grid pattern:
role="grid"(labeled with the visible month and year), a leadingrole="row"of sevenrole="columnheader"weekday labels, then onerole="row"per week withrole="gridcell"per day. Every week row always has exactly 7 gridcells — leading/trailing blanks before the 1st and after the last day are real cells witharia-disabled="true"(notaria-hidden), specifically so the row stays structurally valid rather than short a cell. - The weekday labels (
Sun–Sat) are what let a screen reader announce which column a day falls in, and what lets a sighted reader tell the columns apart without inferring it from the run of blanks before the 1st. They are formatted fromDatewith an expliciten-USlocale, the same one the month label and the field's displayed value use. - The selected day's gridcell carries
aria-selected="true"(and only that one); today's date getsaria-current="date"on its button. These are independent — "selected" and "today" can be different days, or the same one. - Keyboard support is partial, and it's worth being precise about the gap: every day is a
real, individually focusable
<button>, soTab/Shift+Tabwalk through the visible month's days in order andEnter/Spaceactivate the focused one — but there's no roving tabindex and noArrowLeft/ArrowRight/ArrowUp/ArrowDowngrid navigation, and noEscapeto close the calendar. Plan keyboard testing around Tab and Enter/Space, not the full ARIA grid keyboard convention. - Clicking the (read-only) text input toggles the calendar open or closed. Clicking a day calls
onValueChangeand closes the calendar. Clicking anywhere outside the control closes it too. - "Previous month"/"Next month" have explicit
aria-labels and correctly roll over year boundaries in both directions. - The popover is positioned with plain CSS directly below the input — there's no viewport-collision handling, so give it room to render below the field.
Do
- Store the value from onValueChange in state you control — the field is fully controlled, with no internal fallback.
- Give it room below the input — the popover always opens downward with no flip-to-fit logic.
- Use it for a single, nearby date where seeing the calendar actually helps.
- Pair it with a real label — it becomes the trigger input’s accessible name.
Don't
- Expect Escape to close the calendar, or arrow keys to move between days — neither is implemented.
- Use it for a date far from today (a birth year, say) — there’s no month/year jump, only Prev/Next.
- Type directly into the field — it’s read-only by design; selection happens through the grid.
- Reach for it when you need a date range — this picker is single-date only.