Accessible Calendar and Timeline Views
Permalink to "Accessible Calendar and Timeline Views"Calendar and timeline views turn date data into space: a month grid of bookings, a week schedule of shifts, a Gantt chart of project tasks, a resource timeline of machines. The layout carries the meaning — overlap, sequence, gaps — and that is exactly what screen reader users cannot see. Many calendar components render events as absolutely positioned divs with no structure, so a screen reader reads a jumble of titles with no dates. Most also make dragging the only way to move or resize an event.
This guide belongs to accessible date & time data.
Spec reference
Permalink to "Spec reference"- SC 1.3.1 Info and Relationships (A) — the relationship between an event and its date and time must be programmatic: an event inside a day cell with a header, or an event whose name includes its time.
- SC 1.3.2 Meaningful Sequence (A) — DOM order should follow chronological order even if events are positioned visually.
- SC 2.1.1 Keyboard (A) — navigation, selection, creation and editing by keyboard.
- SC 2.5.7 Dragging Movements (AA) — moving and resizing events need single-pointer alternatives (edit dialog, menu).
- SC 1.4.1 Use of Color (A) — event categories and statuses need text, not just colour.
A month view maps naturally to role="grid" with weekday column headers. Week and timeline views are better served by a list of days, each containing a list of events, with the visual timeline as presentation.
When to use — and when not to
Permalink to "When to use — and when not to"Use calendar or timeline views when spatial overview matters — finding gaps, spotting overlaps, seeing a project’s critical path. Always provide an agenda or table view of the same data; for many tasks (what is next, what is on Friday) it is faster for everyone, and it is the primary view for many screen reader users.
Do not encode information only in position or length. A Gantt bar’s length is its duration; say the duration in the bar’s name. The misapplication: a resource timeline where the only way to discover a conflict is to see two bars overlap.
Annotated code example
Permalink to "Annotated code example"<!-- Month view: grid of days; events listed inside each day -->
<table role="grid" aria-labelledby="month-h">
<caption id="month-h">October 2026</caption>
<thead><tr><th scope="col" abbr="Monday">Mon</th>…</tr></thead>
<tbody>
<tr>
<!-- Day cell name: date and count (SC 1.3.1) -->
<td role="gridcell" tabindex="0" aria-label="Friday 2 October, 3 events">
<span aria-hidden="true">2</span>
<ul class="events">
<!-- Event name includes time, duration and category (SC 1.3.1, 1.4.1) -->
<li><a href="/events/41">Design review, 10:00 to 11:00, internal</a></li>
<li><a href="/events/42">Lund & Co call, 13:30 to 14:00, customer</a></li>
<li><a href="/events/43">Release, all day</a></li>
</ul>
</td>
</tr>
</tbody>
</table>
<a href="?view=agenda">Switch to agenda view</a> <!-- equivalent alternative -->
// Grid navigation by day and week; Enter moves into the day's event list
cal.addEventListener('keydown', (e) => {
const step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[e.key];
if (step) { e.preventDefault(); focusDay(addDays(current, step)); }
if (e.key === 'PageUp' || e.key === 'PageDown') { e.preventDefault(); focusDay(addMonths(current, e.key === 'PageUp' ? -1 : 1)); }
if (e.key === 'Enter') { e.preventDefault(); activeCell().querySelector('.events a')?.focus(); }
if (e.key === 'Escape' && e.target.closest('.events')) { e.preventDefault(); activeCell().focus(); }
});
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Key | Action | Expected announcement | AT deviations |
|---|---|---|---|
| Arrows | Move by day / week | “Friday 2 October, 3 events” | Column header (weekday) read on change by some readers |
| Page Up / Down | Previous / next month | “October 2026” then the day | — |
| Enter | Into the day’s events | “Design review, 10:00 to 11:00, internal, link” | — |
| Tab (inside day) | Next event | Event name | — |
| Escape | Back to the day cell | Day name | — |
| E or Enter on an event | Open edit dialog (move or resize) | Dialog with start and end fields | — |
Integration context
Permalink to "Integration context"Date picking inside events uses the date picker grid keyboard pattern. Moving events without dragging mirrors keyboard row reordering with move buttons. Gantt task lists are treegrids — see building an accessible treegrid with expandable rows. Event durations follow formatting durations for screen readers.
Gotchas
Permalink to "Gotchas"Overflow events. “+3 more” links must say which day and how many: “Show 3 more events on 8 October”.
All-day and multi-day events. An event spanning several days should appear once in each day’s list with “day 2 of 3” in its name, or once with its range — not as an unlabelled bar crossing cells.
Time zones. Event times shown in the viewer’s zone need the zone stated once, in the view’s heading or toolbar, as in announcing time zones in data tables.
Framework notes
Permalink to "Framework notes"FullCalendar renders month views as tables and supports keyboard focus on events, but drag is the default way to move; add an edit dialog with start and end fields, and enable listWeek or listMonth views as agenda alternatives. React Big Calendar has an agenda view; its month view needs extra ARIA work. Gantt libraries vary widely; prefer ones that expose a task table and test keyboard access before adopting.
// FullCalendar: include an agenda alternative in the header toolbar
new Calendar(el, { headerToolbar: { right: 'dayGridMonth,timeGridWeek,listMonth' }, eventStartEditable: true });
Worked example
Permalink to "Worked example"A clinic’s appointment calendar showed a week grid of coloured blocks. A receptionist using JAWS could not find free slots and booked over existing appointments by mistake. The fix added an agenda view as the default for her account, named every block “Room 2, 09:00 to 09:30, follow-up, Ana P.” with the category in words, made each day a list in time order, and replaced drag-to-move with an “Edit time” dialog. Free slots are listed as items too (“Room 2, 09:30 to 10:00, free”), so finding gaps no longer depends on seeing white space.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Separate the calendar’s data model from its layouts so month, week, timeline and agenda views render the same events with the same names. Provide event-name templates (title, time range, category, status) and require an agenda view in every calendar instance.
FAQ
Permalink to "FAQ"Should a month view be a table or a grid?
If users only read it, a table works. If they move focus between days and act on them, use role=“grid” so screen readers pass arrow keys through.
Is an agenda view enough on its own?
For many users and tasks, yes, and it should be easy to choose. The visual calendar remains useful for overview, so keep both.
How do I make a Gantt chart accessible?
Pair it with a task table or treegrid that includes start, end, duration, dependencies and status. The bars become a visual summary of the table.
How should overlapping events be described?
In words: “Overlaps with Lund & Co call” in the event’s description, and a conflict count in the day cell.
Should free time be listed?
In scheduling tools, yes — as items in the agenda or day list. Free time is information users are looking for.
Related
Permalink to "Related"- Date picker grid — the small calendar
- Treegrid with expandable rows — Gantt task structure
- Move buttons for reordering — moving without dragging