Overview
Data tables are essential for presenting structured information like inventories, schedules, and reports. When built with proper <table> semantics, screen readers can navigate cell-by-cell and announce the relevant column and row headers for each cell. When tables are faked with <div> grids, all of that context is lost — users hear a flat stream of text with no way to understand which value belongs to which column.
WCAG Criteria:
- 1.3.1 Info and Relationships — table structure and header associations must be programmatically determinable
- 4.1.2 Name, Role, Value — interactive controls (like sort buttons) must expose their name, role, and current state
Key requirements:
- Never use ARIA
role="grid"just to make rows clickable — it changes the keyboard model entirely <caption>is like a heading for the table — it is announced when the user enters the tablescope="col"/scope="row"tell screen readers which header applies to which cells- Sort indicators should be programmatic (
aria-sort), not visual-only
Semantic Table
Semantic Table vs. Div Grid
Inaccessible
<!-- Div grid — screen reader sees flat text, no table structure -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;">
<div>Product</div>
<div>Price</div>
<div>Stock</div>
<div>Widget A</div>
<div>$9.99</div>
<div>142</div>
<div>Widget B</div>
<div>$14.50</div>
<div>87</div>
<div>Widget C</div>
<div>$24.00</div>
<div>231</div>
</div>Live Preview
Product
Price
Stock
Widget A
$9.99
142
Widget B
$14.50
87
Widget C
$24.00
231
Accessible
<table>
<caption>Product Inventory</caption>
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col">Price</th>
<th scope="col">Stock</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Widget A</th>
<td>$9.99</td>
<td>142</td>
</tr>
<tr>
<th scope="row">Widget B</th>
<td>$14.50</td>
<td>87</td>
</tr>
<tr>
<th scope="row">Widget C</th>
<td>$24.00</td>
<td>231</td>
</tr>
</tbody>
</table>Live Preview
| Product | Price | Stock |
|---|---|---|
| Widget A | $9.99 | 142 |
| Widget B | $14.50 | 87 |
| Widget C | $24.00 | 231 |
What’s wrong with the div grid?
- Screen readers cannot identify the structure as a table — there are no rows, columns, or headers
- Users cannot navigate cell-by-cell with table shortcuts (Ctrl+Alt+Arrow keys in most screen readers)
- When landing on “$9.99” in the div version, users have no idea which product or column it belongs to
- The visual grid layout is purely cosmetic — none of that structure reaches assistive technology
What the screen reader announces:
| Version | Announcement when navigating to “$9.99” |
|---|---|
Inaccessible (<div> grid) | “$9.99” (no context — which product? which column?) |
Accessible (<table>) | “Price column, Widget A row, $9.99” |
Sortable Table
Sortable Table with aria-sort
Inaccessible
<!-- onclick on the th itself — no button, no aria-sort -->
<table>
<thead>
<tr>
<th>Product</th>
<th onclick="sortTable()" style="cursor:pointer;">
Price ▲
</th>
<th>Stock</th>
</tr>
</thead>
<tbody>
<tr><td>Widget A</td><td>$9.99</td><td>142</td></tr>
<tr><td>Widget B</td><td>$14.50</td><td>87</td></tr>
<tr><td>Widget C</td><td>$24.00</td><td>231</td></tr>
</tbody>
</table>Live Preview
| Product | Price ▲ | Stock |
|---|---|---|
| Widget A | $9.99 | 142 |
| Widget B | $14.50 | 87 |
| Widget C | $24.00 | 231 |
Accessible
<table>
<caption>Product Inventory</caption>
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col" aria-sort="ascending">
<button onclick="toggleSort(this)">
Price <span aria-hidden="true">▲</span>
</button>
</th>
<th scope="col">Stock</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Widget A</th>
<td>$9.99</td><td>142</td>
</tr>
<tr>
<th scope="row">Widget B</th>
<td>$14.50</td><td>87</td>
</tr>
<tr>
<th scope="row">Widget C</th>
<td>$24.00</td><td>231</td>
</tr>
</tbody>
</table>Live Preview
| Product | Stock | |
|---|---|---|
| Widget A | $9.99 | 142 |
| Widget B | $14.50 | 87 |
| Widget C | $24.00 | 231 |
What’s wrong with the inaccessible sort?
- The
<th>withonclickis not focusable by keyboard — users cannot Tab to it - Even if they could reach it,
<th>is not a button and cannot be activated with Enter or Space - The visual arrow (“Price ▲”) is the only sort indicator — screen readers do not convey it as a sort state
- Without
aria-sort, assistive technology has no way to tell the user which column is sorted or in which direction
What the screen reader announces:
| Version | Announcement on the Price column header |
|---|---|
Inaccessible (onclick on <th>) | “Price ▲, column header” (arrow is just text, no sort state) |
Accessible (<button> + aria-sort) | “Price, button, column header, sorted ascending” |
| After clicking to toggle | ”Price, button, column header, sorted descending” |