Accessibility
How the calendar works with a keyboard and a screen reader, every key, every announcement, and what is checked automatically.
Accessibility lives in the hooks, so the registry's markup and markup of your own get the same
roles, names, keys and announcements. The accessibility contract states
what markup must keep, and two tools check it: a development checker and runConformance
(Testing). axe runs on every example with no rule disabled.
The model
A view is a grid (time and day grids) or a list (the list view), named by the toolbar's
title. Inside it, focus moves in roving composites: one tab stop each, the arrow keys move
inside.
- The events: every event and "+N more" of the view, in time order. ↑ and ↓ walk them in the order a screen reader reads them.
- The dates: in a month, the day cells. In a week or day view that takes selections or date clicks, the slot cursor: one element that stands on one slot at a time and is a new element for every slot, so a screen reader announces each move.
- The header links of a time grid with
navLinks.
So a view has at most three tab stops, however many events it holds. The toolbar's buttons and its view switcher (a radio group: one tab stop, the arrow keys switch views) come before the view.
Keys
Events (any view):
| Key | Does |
|---|---|
| ↑ / ↓ | the previous / next event or "+N more", in time order |
| ← / → | the first event of the previous / next day that has events |
| Home / End | the first / last event of the view |
| PageUp / PageDown | the previous / next period; focus follows to its first event |
| Enter | the event's own action: its link, or onEventClick |
| Escape | the day cell (month), or the slot cursor (week, day) where the event starts |
| Space | grabs the event to move it, when it can be edited (Editing) |
| Shift+F10, ContextMenu | opens its actions menu (Event actions) |
The slot cursor (week and day views) and day cells (month):
| Key | Slot cursor | Day cell |
|---|---|---|
| ↑ / ↓ | the previous / next slot (from the first slot ↑ goes to the all-day row) | a week earlier / later |
| ← / → | the same time on the previous / next day | the previous / next day |
| Home / End | the day's first / last slot | the week's first / last day |
| Ctrl+Home / Ctrl+End | the grid's first / last slot | the grid's first / last day |
| PageUp / PageDown | the previous / next period | the previous / next period |
| Space | starts a selection | starts a selection |
| Shift + a move key | starts a selection and extends it | the same |
| Enter | onDateClick, else starts a selection | its nav link, else onDateClick |
| Escape | clears the selection | clears the selection |
While selecting, the move keys extend the selection, Enter or Space selects, and Escape cancels. A grabbed event has its own keys, on Editing; placing ("Move to…", or an item picked up from outside) uses the slot cursor's and day cells' moves with Enter or Space to place and Escape to cancel.
In right-to-left layouts ← and → swap meaning everywhere, so they always move the way they point.
Names and descriptions
- An event's name is its visible content, time first ("09:00 – 10:30 Haircut · Anna"). Its description gives what the screen does not: the full date and time and its resources ("Thursday, October 22, 2026, 09:00 to 10:30, Chair 2"), and, when it can be edited, how ("Press Space to move").
- A day cell is named by its date and its number of events ("Thursday, October 22, 2026, 3 events"); the visible day number is hidden from screen readers.
- The slot cursor names its slot and its verdict: "Monday, October 19, 2026, 09:00 to 09:30, available", or "unavailable" with the reason. In the hour that happens twice on a DST day, times carry their offset ("02:30 (UTC+1)").
- A background event with a
labelis an image with that name; one without is hidden. - Toolbar buttons are named by what they do ("Previous Week"), the view radios by their view ("Week view").
A grid says aria-readonly="true" only when nothing in it can change: it takes no selection,
none of its events can be moved or resized (an editing engine and an event in the view whose
editable flags allow a change), and it takes no drops from outside (a calendar inside
<CalendarExternalDrops> is not read-only). A month that takes selections is
aria-multiselectable with aria-selected on every day cell; a week or day view that does
carries data-selectable, which the contract checker and runConformance read
(slot-cursor).
The calendar's root carries lang from its strings (strings.lang: 'en' for the built-in
English, 'sv' for the Swedish pack), not from locale, and so do its popover, menus and
dialogs, so a calendar in another language than the page's is read in its own, and one with
Hebrew dates but English texts (locale="he-IL", no Hebrew strings) is read as English
(Languages).
Announcements
A live region inside the calendar says what changed where focus does not show it:
| When | Screen readers hear |
|---|---|
| a navigation | the new period ("Week of 19–25 October 2026") |
| a selection starts, grows, ends | "Selecting from …", "Selecting …", "Selected …", "Selection cancelled" |
| a selection the rules refuse | "Can't select …" and the reason |
| an event is grabbed, moved, dropped | "Grabbed … (the keys)", "Moving to …", "Moved … to …", "Change cancelled" |
| a change your app rejects | "Change reverted" |
| "Move to…", an item picked up | "Moving Haircut · Ben. Choose a time with the arrow keys and press Enter, or click a time." |
| a placing step onto a day (month) | "available", or "unavailable" and the reason (a slot cursor's name says it in a week) |
| an item dropped from outside | "Dropped … on …", or "Drop reverted" |
Steps are announced after a short pause (150 ms), so holding an arrow key does not queue a
sentence per slot. Refusals and reverts are assertive; the rest is polite. announcer: 'auto'
speaks through the browser's ariaNotify where it exists, and commands.announce(message) lets
your app use the same channel.
Focus
Focus is never lost to the page body: after a drop, a revert, a navigation or new data, it follows the event to where it is drawn, else to the closest thing that still exists ("+N more", the view's tab stop). Menus and dialogs return focus to the event that opened them. Focused elements are never hidden under a sticky header.
Pointers and touch
Every drag has a single-pointer alternative (Event actions), touch has a long-press with select-then-drag, and pointer gestures never steal focus. Resize handles are smaller than 24 by 24 px on short events: they rely on WCAG 2.5.8's equivalent exception, since the actions menu and the keyboard resize too.
Manual testing
Automated checks cannot hear a screen reader. The manual passes have not run yet: with NVDA and Firefox, JAWS and Chrome, and VoiceOver and Safari, the audit of the alternatives to dragging (WCAG 2.5.7), and on real iOS and Android devices. They run before the first public release, and their results will be published here.