Skip to main content

Calendar

Implemented

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

Categorymolecules
Sourcecomponents/molecules/Calendar/Calendar.tsx
AI HintStandalone interactive month grid for date selection surfaces (e.g. booking, schedule pickers). Labels are English by default — pass the label props for localized UIs. Prefer DatePicker for form date entry (native locale widget, ISO strings); prefer StatisticsDateFilter for from/to range filtering. Zero consumers as of 2026-08-28.

Standalone 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)

November 2026
Mo
Tu
We
Th
Fr
Sa
Su

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

November 2026
Mo
Tu
We
Th
Fr
Sa
Su

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

Novembre 2026
Lu
Ma
Me
Je
Ve
Sa
Di

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 7 columnheaders plus six week rows of 7 gridcells each (rows styled display: contents, visual layout unchanged). The grid is named by the aria-live="polite" month header via aria-labelledby; the outer wrapper is role="group" named by ariaLabel.
  • Roving tabindex — single tab stop among day cells: ArrowLeft/Right ±1 day, ArrowUp/Down ±7 days, Home/End week bounds (Monday–Sunday), PageUp/PageDown ±1 month (day-clamped), Enter/Space select. 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 are aria-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

Prop
Type
Default
Description
value
Date
—

Selected 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) => void
—

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

minDate
Date
—

Dates before this are disabled (compared at start-of-day).

maxDate
Date
—

Dates after this are disabled (compared at start-of-day).

disabledDates
Date[]
—

Individual dates to disable (matched by year/month/day).

showOutsideDays
boolean
true

true (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).

ariaLabel
string
Calendar

Accessible name for the outer role="group" wrapper.

id
string
cal-<useId>

Explicit id — the month header id derives from it (${id}-label) and keys the aria-labelledby wiring between the header and the grid.

weekdayLabels
string[]
["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]

Weekday header labels, Monday-first order (7 entries). English defaults; pass your own strings for localization.

monthLabels
string[]
["January", …, "December"]

Month names, January-first order (12 entries); rendered in the aria-live month header.

prevMonthLabel
string
Previous month

Accessible name for the previous-month navigation button.

nextMonthLabel
string
Next month

Accessible name for the next-month navigation button.

Props

Preview

Calendar

Prop
Type
Required
Default
Description
value
Date
Optional
—

Selected date; re-syncs the viewed month when it changes.

onChange
(date: Date) => void
Required
—

Called when a day is selected (click, Enter or Space).

minDate
Date
Optional
—

Dates before this are disabled (non-selectable).

maxDate
Date
Optional
—

Dates after this are disabled (non-selectable).

disabledDates
Date[]
Optional
—

Individual disabled dates (compared by calendar day).

showOutsideDays
boolean
Optional
true

Render trailing/leading days of adjacent months. When false, outside cells become aria-hidden non-focusable placeholders that keep week alignment.

ariaLabel
string
Optional
'Calendar'

Accessible name for the calendar group.

id
string
Optional
'cal-' + useId()

Stable id base; keys the month-label element.

selectionMode
'single' | 'range'
Optional
'single'

Range mode drives selection through range/onChangeRange instead of value/onChange (single mode is unchanged).

range
{ start?: Date; end?: Date }
Optional
—

Range-mode value. { start } = start picked, awaiting the end; { start, end } = complete (or single-day) range.

onChangeRange
(range: { start?: Date; end?: Date }) => void
Optional
—

Range-mode callback. Later click completes the range, earlier click restarts it, same-day click completes a single-day range.

weekdayLabels
string[]
Optional
["Mo"…"Su"]

Weekday header labels, Monday-first order (7 items). Override for localization.

monthLabels
string[]
Optional
["January"…"December"]

Month names, January-first order (12 items).

prevMonthLabel
string
Optional
'Previous month'

Accessible name for the previous-month button.

nextMonthLabel
string
Optional
'Next month'

Accessible name for the next-month button.

Quick use:<Calendar />

Accessibility

ARIA rolegrid

ARIA

1 rule

WAI-ARIA APG Calendar

Valid grid structure: role='grid' > role='row' (header row of 7 columnheader + 6 week rows) > role='gridcell' day buttons; month label is aria-live='polite'

Keyboard

1 rule

WCAG 2.1.1 / APG Grid Pattern

Roving tabindex — single tab stop; ArrowLeft/Right ±1 day, ArrowUp/Down ±7 days, Home/End week bounds, PageUp/PageDown ±1 month (day-clamped), Enter/Space selects

i18n

1 rule

WCAG 3.1.2

All visible and accessible labels overridable via weekdayLabels/monthLabels/prevMonthLabel/nextMonthLabel/ariaLabel; defaults are English

validation

1 rule

APG Disabled Cells

minDate/maxDate/disabledDates disable day cells (aria-disabled + disabled); disabled cells receive roving focus but cannot be selected

Usage

Import

tsxSnippet
import { Calendar } from "@epasslive/design-system"

Example 1

tsxSnippet
<Calendar value={selected} onChange={setSelected} minDate={today} disabledDates={holidays} />

Example 2

tsxSnippet
<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

Design System v4.1.0 | ePassLive