DatePicker

Popover date grid using the ARIA grid pattern.

organismsSource
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.

January 2026
Sun
Mon
Tue
Wed
Thu
Fri
Sat
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

PropTypeDefaultDescription
labelrequiredstringAccessible label for the trigger input (also its accessible name).
onValueChangerequired(date: Date) => void
classNamestring
defaultOpenbooleanfalseRenders 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.
valueDate

Accessibility

  • The calendar uses the WAI-ARIA grid pattern: role="grid" (labeled with the visible month and year), a leading role="row" of seven role="columnheader" weekday labels, then one role="row" per week with role="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 with aria-disabled="true" (not aria-hidden), specifically so the row stays structurally valid rather than short a cell.
  • The weekday labels (SunSat) 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 from Date with an explicit en-US locale, 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 gets aria-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>, so Tab/Shift+Tab walk through the visible month's days in order and Enter/Space activate the focused one — but there's no roving tabindex and no ArrowLeft/ArrowRight/ArrowUp/ArrowDown grid navigation, and no Escape to 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 onValueChange and 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.