Skip to Content
Component ExamplesData Tables

Data Tables

Sortable, structured data with proper header associations and caption

Maintained by Martin KrugerLast reviewed

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
Product
Price
Stock
Widget A
$9.99
142
Widget B
$14.50
87
Widget C
$24.00
231
View inaccessible code
<!-- 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>
Accessible
Product Inventory
Product Price Stock
Widget A $9.99 142
Widget B $14.50 87
Widget C $24.00 231
View accessible code
<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>

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

Predicted semantic summary (exact output varies):

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
Product Price ▲ Stock
Widget A $9.99 142
Widget B $14.50 87
Widget C $24.00 231
View inaccessible code
<!-- 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>
Accessible
Product Inventory
Product Stock
Widget A $9.99 142
Widget B $14.50 87
Widget C $24.00 231
View accessible code
<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>

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

Predicted semantic summary (exact output varies):

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”

Table design decisions

Give every data table a descriptive <caption>; use nearby headings for broader page structure. Simple tables can associate headers with scope. For irregular, multi-level headers, simplify the table where possible or use unique id values and headers references deliberately. Responsive presentation must preserve relationships: horizontal scrolling is often safer than converting cells into unrelated visual cards, while a card view needs equivalent labels for every value. Do not use table markup for layout.


Resources