Back to Home

Accessible DatePicker React WCAG 2.2

The article describes creating WCAG 2.2 AA DatePicker on React with AI-generation fixes. Covers deviations from APG, focus management, aria-live assertive. For senior developers.

WCAG DatePicker on React: from AI to ideal
Advertisement 728x90

Building an Accessible React DatePicker to WCAG 2.2 AA Standards

Our dev team started with Claude-generated DatePicker code based on the WAI-ARIA APG "Date Picker Dialog" pattern. The initial prompt specified WCAG 2.1/2.2 Level AA requirements: input with aria-describedby, trigger button, modal dialog with role="dialog", and a grid-based calendar table. However, the raw version revealed critical issues with focus, navigation, and accessibility.

The AI implemented basic keyboard navigation (arrow keys, Home/End, PageUp/Down, Enter/Space, Escape), roving tabindex on gridcell elements, and aria-live="polite" for month changes. But testing with VoiceOver and NVDA exposed problems: focus didn't land on the selected date when opening, aria-live announcements queued up, and the dialog flickered on repeated clicks.

Deviations from APG for Real-World Accessibility

The APG spec calls for <td role="gridcell"> without nested buttons, relying on manual onClick/onKeyDown and tabindex. Real-world testing showed clear advantages to using <button> inside <td>:

Google AdInline article slot
  • Native focus handling and Enter/Space support without extra code.
  • Automatic semantics for screen readers.
  • Built-in disabled state without aria-disabled or preventDefault.
  • Reliable click events from mouse or keyboard.

This was a deliberate choice: APG is a guide, but device testing takes priority. We ensured ≥4.5:1 contrast, used CSS Modules, and native Date/Intl APIs for formatting—no external dependencies beyond React/TypeScript.

Fixing Functional and UX Issues

The raw code wouldn't compile: missing index.html and TypeScript errors. After fixes, more issues surfaced:

Functional bugs:

Google AdInline article slot
  • Dates outside minDate/maxDate jumped to another month instead of showing disabled cells.
  • No way to clear the input.

Visual problems:

  • Missing focus styles on cells.
  • Uneven td sizing.

Accessibility:

  • No focus on selected date when opening—screen readers stayed silent.
  • aria-live="polite" on month header caused announcement queues during fast navigation.
  • Blur + click on trigger caused dialog flicker.

Fixes:

Google AdInline article slot
  • On open, focus lands on value or today; screen reader announces context (weekday, date, month, year).
  • aria-live="assertive" in a separate hidden region for instant month announcements.
  • After selection, focus returns to trigger with updated aria-label.
  • Show value's month even with disabled days.
  • Removed OK/Cancel: Enter confirms, Esc closes.

Component Structure and Keyboard Navigation

Props: value (Date | null), onChange, minDate?, maxDate?, disabledDates?, locale?.

Key ARIA:

  • Input: aria-describedby for format, readonly.
  • Trigger: dynamic aria-label ("Selected: MM/DD/YYYY" or "Pick a date").
  • Dialog: role="dialog", aria-modal="true", focus trap.
  • Grid: table role="grid", td with button role="gridcell", aria-selected on value, aria-disabled on unavailable dates.
  • Month header: aria-live="assertive" in polite region.

Navigation:

  • Arrow keys: adjacent days.
  • Home/End: first/last day of month.
  • PageUp/Down: ±1 month.
  • Shift+PageUp/Down: ±1 year.
  • Enter/Space: select.
  • Escape: close.

Key Takeaways

  • AI speeds up the skeleton (ARIA, navigation), but screen reader testing is essential.
  • <button> inside <td> is more reliable than pure gridcell in real browsers + screen readers.
  • aria-live="assertive" prevents queues during month navigation.
  • Focus management: open on value/today, return to trigger.
  • Minimal stack: React + TS, no libraries.

The component is production-ready as a single-date picker, with open code for extensions (years, ranges).

— Editorial Team

Advertisement 728x90

Read Next