Dynamic content and ARIA
Make data tables understandable
Use captions, header cells, scopes, and simple structure so schedule relationships remain available.
A data table encodes relationships. “Main hall” means nothing on its own. “Room: Main hall” for “Shipping Astro islands” at 10:00 is the information. Sighted users read those relationships from the grid lines. Screen readers read them from the markup, and only if the markup has them. That’s WCAG 1.3.1 Info and Relationships, Level A, applied to tables.
The flawed schedule is a CSS grid of div elements. It looks like a table. It has no rows, no columns, no headers, and no name.
Use a real table
Here’s the repaired schedule:
<table>
<caption>Schedule, 14 October 2026</caption>
<thead>
<tr>
<th scope="col">Session</th>
<th scope="col">Time</th>
<th scope="col">Room</th>
<th scope="col">Speaker</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Shipping Astro islands</th>
<td>10:00</td>
<td>Main hall</td>
<td>Sara Conti</td>
</tr>
<tr>
<th scope="row">Accessible forms in practice</th>
<td>11:15</td>
<td>Room B</td>
<td>Marco Ferri</td>
</tr>
</tbody>
</table>
Three things do the work. The caption names the table, so the user knows what it is before entering it. th scope="col" marks the column headers. th scope="row" marks the session name as the header of its row, because that’s how people think about a schedule: “when is the forms talk?”.
Listen to it
Turn on VoiceOver and move onto the table:
Schedule, 14 October 2026, table, 3 rows, 4 columns
Now navigate cell by cell with VO + arrow keys. Moving right along the first row you hear “Time, 10:00”, then “Room, Main hall”, then “Speaker, Sara Conti”. Move down from the Time column and you hear “Accessible forms in practice, 11:15”. Every value arrives with its header. On the div grid the same keys read “10:00”, “Main hall”, “Sara Conti” with no context at all, and there is no table to enter in the first place.
Keep it simple
The temptation with a schedule is to merge cells: two rooms spanning one time slot, a lunch row across everything, a “Morning” header above two other headers. Each merge makes the header relationship harder to compute, and some screen readers give up.
A single colspan for the lunch row is fine:
<tr>
<td colspan="4">Lunch, 12:30 to 13:30</td>
</tr>
Two levels of column headers are not fine. Split the table in two, one per room, each with its own caption. If you truly can’t split, give each header an id and each cell a headers attribute listing them. Since that’s the complexity we’re trying to avoid, treat it as the last resort.
Layout tables and the reverse mistake
The opposite error still shows up: a table used to lay out the page columns. Screen readers then announce “table, 1 row, 3 columns” on a page with no data in it, and the table navigation commands stop at nonsense. Use CSS grid for layout, table for data. Each element says what it is.
Try this on one table in your project: navigate it with the screen reader’s table commands and check that every data cell is announced with the right header. Any cell that arrives alone is a finding.
Lesson completed