Accessible Tables
Data tables vs layout tables — th scope, captions, headers, responsive strategies, and when to use grids instead.
- accessibility
- accessible-tables
Screen reader users navigate data tables by cell, hearing row/column headers. If you fake a table with divs or use tables for layout, that navigation breaks. Use <table> for tabular data; use CSS layout for page structure.
Docs: WAI Tables, MDN table.
Minimal data table
<table>
<caption>Team members — Q3</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Role</th>
<th scope="col">Location</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Ada Lovelace</th>
<td>Engineer</td>
<td>London</td>
</tr>
<tr>
<th scope="row">Alan Turing</th>
<td>Researcher</td>
<td>Manchester</td>
</tr>
</tbody>
</table>
<caption>— table name (oraria-labelledby)scope="col"/scope="row"— associates headers- Complex tables may need
headers+id
Layout tables: don’t
<!-- Bad page layout -->
<table>
<tr>
<td>nav</td>
<td>content</td>
</tr>
</table>
Use flex/grid. If you must support legacy, role="presentation" on layout tables removes table semantics — still prefer real layout CSS.
Sortable columns
<th scope="col">
<button type="button" aria-label="Sort by Name, currently ascending">
Name
<span aria-hidden="true">▲</span>
</button>
</th>
Announce sort changes via live region or button name updates.
Responsive strategies
| Approach | Notes |
|---|---|
| Horizontal scroll | Keep table; ensure keyboard can scroll container |
| Stacked rows (CSS) | Preserve header association carefully — hard |
| Card list at small breakpoints | Different markup path; not the same table |
| Priority columns | Hide less critical cols with care |
Never strip header associations when restyling.
Interactive cells
Controls inside cells need their own names:
<td>
<button type="button" aria-label="Delete Ada Lovelace">Delete</button>
</td>
Footguns
- Div grids announced as plain text without headers.
- Empty header cells without labels.
- Merged cells without correct
headers. - Tables missing captions when multiple tables exist.
Interview out-loud answer
“Data tables use table/th/td with scope or headers so AT can announce relationships. Captions name the table. I don’t use tables for layout. Sortable headers are real buttons with clear labels.”
Complex headers
When cells need multiple headers (multi-level column groups), use id + headers. Prefer simpler table shapes when product allows — complex associations are easy to break in refactors.
CSV export as an a11y affordance
For dense analytics tables, offer Download CSV next to the grid. It’s not a replacement for an accessible table, but it gives AT and power users a robust alternative for analysis.
Virtualized tables
Windowing libraries recycle DOM rows. Keep a real header row, don’t lose focus on recycle, and keep any exposed row counts honest. Test keyboard scroll plus SR cell reading on the chosen library.
Sorting and live updates
When sort changes, update the active header’s accessible name (“Name, sorted ascending”) and consider a polite status: “Sorted by Name ascending.” Don’t rely on visual carets alone.
Empty and loading tables
Announce empty states in the caption or a status region: “No invoices match these filters.” For loading, either keep headers visible with a status “Loading rows” or swap to a labeled progress region — don’t leave a silent blank grid.
Extra practice
Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.
Related on this site
- Accessible names computation
- Heading hierarchy for a11y
- What is the accessibility tree
- WCAG principles POUR
- Keyboard accessibility checklist
Further reading
Related guides
- Accessible Combobox PatternBuild or evaluate comboboxes with APG keyboard behavior, aria-expanded/activedescendant, and filterable listbox pairing.
- Accessible Forms ErrorsLabel inputs, associate errors with fields, announce failures, and avoid placeholder-only forms that break accessibility.
- Accessible Menus PatternAPG menu and menubar keyboard model — arrow navigation, escape to close, aria-expanded, and when a disclosure is enough.
- Accessible Modals PatternsModal dialogs that work: focus trap, Escape, return focus, aria-modal, and why native dialog or APG patterns beat div soup.
- Accessible Names ComputationHow browsers compute accessible names from content, labels, aria-label, and labelledby — and how that powers Testing Library queries.