Skip to Content
Component ExamplesData Tables

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:

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 table
  • scope="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 Inventory
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:

VersionAnnouncement 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 &#9650; </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">&#9650;</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 Inventory
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> with onclick is 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:

VersionAnnouncement 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”

Resources