Forms & Controls
Date Picker
A dialog-based grid pattern for choosing a date, paired with a text input.
Last verified against WCAG 2.2 and WAI-ARIA APG 1.2 on 2026-07-02.
Implementation
A dialog containing an ARIA grid, with the same focus-trap/Escape/focus-restore logic as the flagship Dialog pattern, plus arrow-key date navigation.
date-picker-pattern.tsx
function onGridKeyDown(e) {
switch (e.key) {
case "ArrowRight": moveFocus(1); break; // next day
case "ArrowLeft": moveFocus(-1); break; // previous day
case "ArrowDown": moveFocus(7); break; // next week
case "ArrowUp": moveFocus(-7); break; // previous week
case "Home": setFocusedDate(startOfWeek(focused)); break;
case "End": /* last day of current week */ break;
case "PageUp": e.shiftKey ? moveYears(-1) : moveMonths(-1); break;
case "PageDown": e.shiftKey ? moveYears(1) : moveMonths(1); break;
case "Enter": case " ": selectDate(focusedDate); break;
}
}
<div role="dialog" aria-modal="true" aria-label="Choose date, July 2026">
<div role="grid" onKeyDown={onGridKeyDown}>
<div role="row">
{week.map(day => (
<button
role="gridcell"
tabIndex={isSameDay(day, focusedDate) ? 0 : -1}
aria-selected={isSameDay(day, selected)}
aria-label={isToday(day) ? `Today, ${formatLong(day)}` : formatLong(day)}
>
{day.getDate()}
</button>
))}
</div>
</div>
</div>
/* Focus trap / Escape / focus-restore-to-trigger reuse the exact same
logic as dialog-pattern.tsx — this popup IS a specialized dialog. */Live demo
Required roles, states & properties
| Element | Attribute | Why |
|---|---|---|
| Popup container | role="dialog" + aria-modal="true" | Same reasoning as the flagship Dialog pattern: identifies the popup and marks background content inert, backed by an explicit focus trap. |
| Popup container | aria-label (month + year) | Gives the popup an accessible name that includes the currently displayed month, since there's no single visible heading element serving that role. |
| Calendar table | role="grid" | Exposes the calendar as a 2D grid so assistive technology's grid navigation commands and cell/row semantics apply. |
| Each week | role="row" | Groups seven date cells into a row, matching the calendar's visual week structure. |
| Each date | role="gridcell" | Marks each date as a selectable grid cell rather than generic content. |
| Selected date | aria-selected="true" | Communicates the currently selected date's state programmatically, not just via a visual highlight. |
| Today's / selected date | aria-label="Today, July 2, 2026" / "Selected, ..." | Adds meaning that would otherwise be conveyed only by a visual ring or dot, so it reaches screen reader users too. |
| Focused date cell only | tabIndex={0} (all others tabIndex={-1}) | Roving tabindex: keeps a single Tab stop for the whole grid — the same approach used by Menu and Tabs on this site — while arrow keys move the active cell. |
Keyboard interaction model
| Key | Behavior |
|---|---|
| Right Arrow | Moves focus to the next day. |
| Left Arrow | Moves focus to the previous day. |
| Down Arrow | Moves focus to the same day one week later (+7 days). |
| Up Arrow | Moves focus to the same day one week earlier (-7 days). |
| Home | Moves focus to the first day (Sunday) of the current week. |
| End | Moves focus to the last day (Saturday) of the current week. |
| Page Up | Moves focus to the same day in the previous month. |
| Shift+Page Up | Moves focus to the same day in the previous year. |
| Page Down | Moves focus to the same day in the next month. |
| Shift+Page Down | Moves focus to the same day in the next year. |
| Enter / Space | Selects the focused date, closes the dialog, and returns focus to the trigger input. |
| Escape | Closes the dialog without changing the selection and returns focus to the trigger. |
| Tab / Shift+Tab | Cycles only within the dialog's focusable elements (month nav buttons, the focused date cell) — trapped exactly as in the Dialog pattern. |
Focus management rules
- On open: focus moves to the selected date's cell, or today's cell if nothing is selected yet — the same "move focus into the dialog" requirement as the Dialog pattern.
- While open: Tab/Shift+Tab cycle only within the dialog's focusable elements (reused focus-trap logic from dialog-pattern.tsx).
- Arrow keys, Home/End, and PageUp/PageDown move a roving tabindex within the grid — only one cell is ever a Tab stop at a time.
- On close (Enter/Space select, Escape, or scrim click): focus returns to the trigger button, exactly as the Dialog pattern restores focus to its trigger.
WCAG 2.2 success criteria mapping
| SC | Name | Level | Why it applies |
|---|---|---|---|
| 2.1.1 | Keyboard | A | All functionality is operable through a keyboard interface with no specific timing. |
| 2.4.3 | Focus Order | A | Focusable components receive focus in an order that preserves meaning and operability. |
| 3.3.2 | Labels or Instructions | A | Labels or instructions are provided when content requires user input. |
| 4.1.2 | Name, Role, Value | A | For all UI components, name, role, and value are programmatically determinable; states and changes are announced. |