Accessible Data Tables
Also Known As
TL;DR
Explanation
Screen readers navigate tables by reading each cell along with its associated headers. Without proper markup, a user hears '42' with no context; with correct markup they hear 'Q3, Revenue, 42'. Key elements: `<table>` for the container, `<caption>` for the table title (announced first), `<thead>` / `<tbody>` / `<tfoot>` for structural sections, `<th>` with `scope='col'` or `scope='row'` for header cells, and `<td>` for data cells. For complex tables with merged cells, `headers` and `id` attributes explicitly associate cells with their headers. The `summary` attribute is obsolete — use `<caption>` or `aria-describedby` instead. Avoid using tables for layout; use CSS Grid or Flexbox. When a table has no natural caption, add `aria-label` or `aria-labelledby` on the `<table>` element.
Common Misconception
Why It Matters
Common Mistakes
- Using `<td>` for header cells instead of `<th>` — screen readers do not know to announce the cell as a header.
- Missing `scope` attribute on `<th>` elements in tables with both row and column headers — causes ambiguous header associations.
- Using `<table>` for page layout — confuses screen reader users who navigate table by table and announce row/column counts.
- Omitting `<caption>` — users cannot identify the table's purpose before entering it.
- Using the obsolete `summary` attribute instead of `<caption>` or `aria-describedby`.
Code Examples
<!-- No headers, no caption, uses td for headers -->
<table>
<tr>
<td>Product</td>
<td>Q1</td>
<td>Q2</td>
</tr>
<tr>
<td>Widgets</td>
<td>120</td>
<td>145</td>
</tr>
</table>
<!-- Fully accessible table -->
<table>
<caption>Quarterly Sales by Product (units)</caption>
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Widgets</th>
<td>120</td>
<td>145</td>
</tr>
<tr>
<th scope="row">Gadgets</th>
<td>98</td>
<td>110</td>
</tr>
</tbody>
</table>