Lesson 5: Keyboard and Accessibility
Keyboard navigation, ARIA roles, and screen reader support.
Lesson 5 of 8: how the calendar handles keyboard input and ARIA semantics, so the date picker is accessible by default.
Built-in keyboard navigation
getDayProps attaches an onKeyDown handler that implements the full WAI-ARIA grid keyboard pattern:
| Key | Action |
|---|---|
| ArrowLeft / ArrowRight | Move focus +/- 1 day |
| ArrowUp / ArrowDown | Move focus +/- 1 week |
| PageUp / PageDown | Move focus +/- 1 month |
| Shift+PageUp / Shift+PageDown | Move focus +/- 1 year |
| Home / End | Move to start/end of week |
| Enter / Space | Select focused date |
| Escape | Dismiss (calls onDismiss) |
Spread getDayProps(day) on each day element and you get all of this.
Focus management
The calendar uses roving tabIndex:
- The focused date has
tabIndex=0. - Other days have
tabIndex=-1. - Tab enters the grid on the focused date, then leaves.
- Arrow keys move focus inside the grid.
Focus crossing a month boundary (e.g. ArrowRight on the last day) auto-advances the displayed month.
ARIA roles
The prop getters apply all required ARIA attributes:
// getGridProps() produces:
{
role: 'grid',
'aria-labelledby': '<header-id>',
'aria-roledescription': 'calendar',
}
// getDayProps(day) produces:
{
role: 'gridcell',
'aria-label': 'Saturday, March 14, 2026',
'aria-selected': true,
'aria-disabled': false,
'aria-current': 'date', // only for today
tabIndex: 0, // or -1
}
// getNavProps('prev') produces:
{
'aria-label': 'Go to previous month',
disabled: false,
onClick: () => { /* navigates */ },
}// getGridProps() produces:
{
role: 'grid',
'aria-labelledby': '<header-id>',
'aria-roledescription': 'calendar',
}
// getDayProps(day) produces:
{
role: 'gridcell',
'aria-label': 'Saturday, March 14, 2026',
'aria-selected': true,
'aria-disabled': false,
'aria-current': 'date', // only for today
tabIndex: 0, // or -1
}
// getNavProps('prev') produces:
{
'aria-label': 'Go to previous month',
disabled: false,
onClick: () => { /* navigates */ },
}Screen reader announcements
The internal useAnnouncer hook adds a visually hidden aria-live="polite" region. It announces:
- Month changes: "March 2026"
- Selection changes: "Selected Saturday, March 14, 2026"
Compound components render the announcer inside Calendar.Root automatically.
When using hooks directly, render the portal yourself:
const { announcer, ...rest } = useCalendar({ adapter, mode: 'single' })
return (
<div>
{/* ... calendar UI ... */}
<announcer.AnnouncerPortal />
</div>
)const { announcer, ...rest } = useCalendar({ adapter, mode: 'single' })
return (
<div>
{/* ... calendar UI ... */}
<announcer.AnnouncerPortal />
</div>
)Testing accessibility
Test keyboard navigation with Tab and arrow keys. Check screen reader output with VoiceOver (macOS) or NVDA (Windows).
Things to verify:
- The grid announces as "calendar" when focused.
- Each day announces its full date.
- Selected days announce "selected".
- Month changes are announced.
- Disabled days announce as disabled.
Next: Lesson 6 - Time Picker