Accessible data tables
Updated October 14, 2026 · 5 min read
Size charts, product specifications, shipping rates, order history, comparison tables — shops have more tables than they think. Screen reader users navigate tables cell by cell and hear the header for each cell, but only if the headers are in the code. This page is part of the accessible UI patterns series.
When to use a table
Use a table when data has a relationship in two dimensions: rows and columns that both mean something (size × measurement, country × shipping cost). Do not use tables for layout, and do not build tabular data from div elements styled as a grid.
The APG table pattern describes a table as a static structure that "is not an interactive widget", and says: "authors are strongly encouraged to use a native HTML table element whenever possible." If many cells contain interactive widgets, the APG grid pattern can reduce the number of tab stops — but a grid is a composite widget with its own arrow-key model and is rarely needed on a shop.
Relevant WCAG criteria
- 1.3.1 Info and Relationships (A): header cells and their data cells are related in the code.
- 1.3.2 Meaningful Sequence (A): reading order makes sense when the table is linearised.
- 1.4.10 Reflow (AA): W3C names data tables (not individual cells) as content that may need two-dimensional scrolling; the rest of the page must still reflow.
- 2.1.1 Keyboard (A): a horizontally scrolling table must be scrollable with the keyboard.
- 4.1.2 Name, Role, Value (A): sort buttons have names and the sort state is exposed.
- 2.4.6 Headings and Labels (AA): captions and headers describe the content.
Code example
<!-- Simple table: headers in the first row and first column -->
<table>
<caption>Size chart for women's jeans, in centimetres</caption>
<thead>
<tr>
<th>Size</th>
<th>Waist</th>
<th>Hip</th>
</tr>
</thead>
<tbody>
<tr><th>S</th><td>66–70</td><td>92–96</td></tr>
<tr><th>M</th><td>71–75</td><td>97–101</td></tr>
</tbody>
</table>
<!-- Sortable column: a button in the header, aria-sort on the header cell -->
<th aria-sort="ascending">
<button type="button">Price</button>
</th>
<!-- Wide table on small screens: a focusable, named scroll container -->
<div class="table-scroll" role="region" aria-label="Shipping rates" tabindex="0">
<table>…</table>
</div>
Notes:
- W3C technique H51 is "using table markup to present tabular information". H63 covers
scope(row,col,rowgroup,colgroup): needed when headers are not in the first row or column; plainthis enough for simple tables with headers in the first row or column. For complex tables with headers on several levels, H43 usesidandheaders. captionnames the table. Do not fake it with a bold first row or a heading above that is not connected.- Per the APG,
aria-sortgoes on the header of the sorted column; set it on one column at a time. - If rows are loaded in pages or virtualised,
aria-rowcountandaria-rowindextell assistive technology the full size.
Keyboard interaction
The APG lists keyboard interaction for the table pattern as "not applicable": a table is not a widget, and its cells are not focusable. Screen readers have their own table navigation commands. Interactive elements inside cells — links, sort buttons, quantity fields — are ordinary tab stops.
| Key | Function |
|---|---|
| Tab | Moves to the next link, button or field inside the table |
| Arrow keys (when the scroll container has focus) | Scroll a wide table horizontally |
| Screen reader table commands | Move cell by cell; the header of each cell is announced |
Common mistakes on online shops
- Size charts as images without a text table, or as tables without any
th. - Specification lists built from
divelements that look like two columns but have no structure — use a table or a description list. - Layout tables from old email-style templates, marked up with
th. - Comparison tables with merged header cells and no
headersattributes. - A first bold row instead of a
captionorth. - Wide tables that overflow the page on mobile, or a scroll container that keyboard users cannot reach.
- Sort links without state, so users do not know the current order.
What automated checks catch vs manual testing
| Automated (axe-core rule) | What it finds |
|---|---|
td-has-header |
Data cells in larger tables without a header |
th-has-data-cells |
Header cells that describe no data cells |
td-headers-attr |
headers attributes that point outside the table |
table-fake-caption |
A first row used as a caption |
empty-table-header |
Header cells with no text (advice) |
scope-attr-valid |
Invalid scope values (advice) |
table-duplicate-name |
Caption and summary with the same text (advice) |
scrollable-region-focusable |
A scrolling container keyboard users cannot reach |
All are cards in our rule library. Manual testing: decide whether each table is data or layout, check that headers are correct and meaningful, navigate with a screen reader's table commands, and check the table on a 320 CSS pixels wide screen.
Run the free one-page check on a product page with a size chart. It is an automated check, not a full audit.
Frequently asked questions
How do I make a table accessible?
Use a real HTML table for tabular data, mark header cells with th, add scope where headers are not simply in the first row or column, give the table a caption, and do not use tables for layout.
When do I need the scope attribute?
W3C technique H63 says simple tables with headers in the first row or column work with plain th elements; scope helps when headers are elsewhere. For complex tables with multi-level headers, use id and headers attributes (H43).
Should I use role="table" or role="grid"?
Prefer the native table element; the APG strongly encourages it whenever possible. A grid is an interactive composite widget with arrow-key navigation, useful only when a table contains many interactive cells.
How do I show which column is sorted?
Put aria-sort="ascending" or "descending" on the header of the sorted column, and make the sort control a button inside the header cell.
Can an automated check test tables?
It finds data cells without headers, headers without data cells, broken headers attributes, invalid scope values and fake captions. It cannot tell whether a table is really a data table or whether the headers make sense.
Sources
- W3C WAI, APG Table pattern: https://www.w3.org/WAI/ARIA/apg/patterns/table/
- W3C, Technique H63 Using the scope attribute: https://www.w3.org/WAI/WCAG22/Techniques/html/H63
- W3C WAI, APG Grid pattern: https://www.w3.org/WAI/ARIA/apg/patterns/grid/
- W3C, Understanding 1.4.10 Reflow: https://www.w3.org/WAI/WCAG22/Understanding/reflow.html