Calendar
ImplementedStandalone month-grid calendar with click-to-select, roving-tabindex keyboard navigation (arrows/PageUp/PageDown/Home/End/Enter), min/max/disabled dates, and overridable labels (English defaults; Monday-first). Optional range mode (selectionMode='range' + range/onChangeRange, hero « Quand » picker, ADR-068 amendment): first click sets the start, same-day second click completes a single-day range, later-day second click completes the range, earlier-day restarts it; hover previews the pending range. For locale-aware date entry use DatePicker (native UA widget); for dashboard range filters use StatisticsDateFilter.
components/molecules/Calendar/Calendar.tsxStandalone inline month grid with click- and keyboard-driven day selection (WAI-ARIA grid, single tab stop, arrow-key navigation), min/max/disabled dates, prev/next month buttons, and customizable weekday/month labels. Monday-first week.
Calendar
Molecule — Form & Choice (Family 3)
The only DS molecule that draws a calendar itself (DatePicker and TimePicker delegate to the browser UA). Renders a permanently visible 42-cell month window — not a popup — as a WAI-ARIA grid with roving-tabindex keyboard navigation. Monday-first week; labels are English by default and customizable via props.
Controlled — value + onChange (required)
Selected: Mon Nov 09 2026. Single tab stop — arrow keys move focus (±1 / ±7 days), Home/End jump to week bounds, PageUp/PageDown change month (day-clamped), Enter/Space selects. Muted outside-month days are selectable and move the view to their month.
minDate / maxDate / disabledDates
Window from today to end of month +3; two individual dates are blacked out. Disabled cells are aria-disabled (kept focusable for roving tabindex, never activatable) and never fire onChange.
Custom labels — French
Same component with French labels via weekdayLabels (Monday-first), monthLabels, prevMonthLabel/nextMonthLabel. All labels are plain props with English defaults — no Intl engine, week is always Monday-first.
Usage
Controlled inline month grid. onChange is required — a bare <Calendar /> renders but throws TypeError on the first day-cell click (or Enter/Space in the grid).
import { Calendar } from '@epasslive/design-system'
import * as React from 'react'
export function EventDate() {
const [date, setDate] = React.useState<Date>(new Date())
return <Calendar value={date} onChange={setDate} ariaLabel="Event date" />
}When to use
- Standalone labelled date field in a form (filters, settings, checkout) → DatePicker: the browser UA owns the calendar popup, keyboard navigation and locale rendering; the value contract is ISO
yyyy-mm-dd. - Custom inline month grid (availability display, event-date explorer, a picker you compose yourself) → Calendar: the only DS molecule that draws the grid itself.
- Date-range filtering in dashboards → StatisticsDateFilter: the shipped range-filter surface. Do not compose Calendar into a range picker, and do not reach for DateRangePicker (planned, defective, no calendar ships).
Localization: English labels are the defaults, but every label is a prop — weekday headers (weekdayLabels, Monday-first), month names (monthLabels) and nav button names (prevMonthLabel/nextMonthLabel). Plain strings only — no Intl engine, week always Monday-first. For fully locale-aware date entry, use DatePicker — the browser UA owns month names, day names and first-day-of-week.
Accessibility
- Valid WAI-ARIA grid:
role="grid"contains one header row of 7columnheaders plus six week rows of 7gridcells each (rows styleddisplay: contents, visual layout unchanged). The grid is named by thearia-live="polite"month header viaaria-labelledby; the outer wrapper isrole="group"named byariaLabel. - Roving tabindex — single tab stop among day cells:
ArrowLeft/Right±1 day,ArrowUp/Down±7 days,Home/Endweek bounds (Monday–Sunday),PageUp/PageDown±1 month (day-clamped),Enter/Spaceselect. DOM focus follows keyboard moves only; crossing a month boundary moves the view; clicking a cell also moves the roving cursor. - Day cells carry
aria-selected; disabled cells arearia-disabled— focusable for roving tabindex but never activatable (APG guidance). WCAG AA contrast in both themes. - Known limitations: labels are plain strings (no Intl engine, week always Monday-first); the ‹ › nav buttons are not gated by
minDate/maxDate; no year-level navigation; day cells use the browser default focus ring.
Props
valueDateSelected date. Highlights the matching day cell; changing value after mount re-syncs the visible month and moves the roving cursor to it.
onChange*(date: Date) => voidFires when a day cell is selected — by click, or Enter/Space on the focused cell. Receives the selected Date normalized to start-of-day; selecting an outside-month day also moves the view to its month.
minDateDateDates before this are disabled (compared at start-of-day).
maxDateDateDates after this are disabled (compared at start-of-day).
disabledDatesDate[]Individual dates to disable (matched by year/month/day).
showOutsideDaysbooleantruetrue (default): outside-month cells render muted and are selectable. false: they become aria-hidden, non-focusable placeholders that preserve the 7-column week alignment (the 42-cell window is unchanged).
ariaLabelstringCalendarAccessible name for the outer role="group" wrapper.
idstringcal-<useId>Explicit id — the month header id derives from it (${id}-label) and keys the aria-labelledby wiring between the header and the grid.
weekdayLabelsstring[]["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]Weekday header labels, Monday-first order (7 entries). English defaults; pass your own strings for localization.
monthLabelsstring[]["January", …, "December"]Month names, January-first order (12 entries); rendered in the aria-live month header.
prevMonthLabelstringPrevious monthAccessible name for the previous-month navigation button.
nextMonthLabelstringNext monthAccessible name for the next-month navigation button.
Props
Calendar
valueDate—Selected date; re-syncs the viewed month when it changes.
onChange(date: Date) => void—Called when a day is selected (click, Enter or Space).
minDateDate—Dates before this are disabled (non-selectable).
maxDateDate—Dates after this are disabled (non-selectable).
disabledDatesDate[]—Individual disabled dates (compared by calendar day).
showOutsideDaysbooleantrueRender trailing/leading days of adjacent months. When false, outside cells become aria-hidden non-focusable placeholders that keep week alignment.
ariaLabelstring'Calendar'Accessible name for the calendar group.
idstring'cal-' + useId()Stable id base; keys the month-label element.
selectionMode'single' | 'range''single'Range mode drives selection through range/onChangeRange instead of value/onChange (single mode is unchanged).
range{ start?: Date; end?: Date }—Range-mode value. { start } = start picked, awaiting the end; { start, end } = complete (or single-day) range.
onChangeRange(range: { start?: Date; end?: Date }) => void—Range-mode callback. Later click completes the range, earlier click restarts it, same-day click completes a single-day range.
weekdayLabelsstring[]["Mo"…"Su"]Weekday header labels, Monday-first order (7 items). Override for localization.
monthLabelsstring[]["January"…"December"]Month names, January-first order (12 items).
prevMonthLabelstring'Previous month'Accessible name for the previous-month button.
nextMonthLabelstring'Next month'Accessible name for the next-month button.
<Calendar />Accessibility
ARIA
1 ruleKeyboard
1 rulei18n
1 rulevalidation
1 ruleUsage
Import
import { Calendar } from "@epasslive/design-system"Example 1
<Calendar value={selected} onChange={setSelected} minDate={today} disabledDates={holidays} />Example 2
<Calendar weekdayLabels={["Lun","Mar","Mer","Jeu","Ven","Sam","Dim"]} monthLabels={MOIS_FR} prevMonthLabel="Mois précédent" nextMonthLabel="Mois suivant" ariaLabel="Calendrier" onChange={setSelected} />Anti-patterns
— Do not use as a form date field — DatePicker (native input) is the sanctioned form control; Calendar is an interactive grid surface
— Do not expect locale/Intl awareness — override labels via the label props instead
— onChange is required — rendering <Calendar /> without a handler throws on interaction