Accessible tables and data
Data tables are among the most challenging elements to make accessible. Without proper markup, screen readers cannot convey the relationship between headers and data cells, making complex data incomprehensible.
Basic accessible table structure
Every data table must have :
- A
<caption>oraria-labelto describe its purpose. <thead>,<tbody>, and optionally<tfoot>to group rows.<th>elements with ascopeattribute for header cells.
<table>
<caption>Quarterly revenue by region (in thousands USD)</caption>
<thead>
<tr>
<th scope="col">Region</th>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
<th scope="col">Q3</th>
<th scope="col">Q4</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">North America</th>
<td>$320</td>
<td>$410</td>
<td>$390</td>
<td>$450</td>
</tr>
<tr>
<th scope="row">Europe</th>
<td>$280</td>
<td>$310</td>
<td>$350</td>
<td>$330</td>
</tr>
</tbody>
</table>How screen readers use this : When a user navigates to a cell like “$410”, the screen reader announces “Q2, North America, $410” — combining the column and row headers. Without
scope, the reader says just “$410”, which is meaningless.
scope vs headers attribute
scope— Works for simple tables with a single level of headers.headers— Required for complex tables with merged cells or multi-level headers.
<!-- Complex table with merged headers -->
<table>
<caption>Sales performance</caption>
<thead>
<tr>
<td rowspan="2"></td>
<th id="h-2024" colspan="2" scope="colgroup">2024</th>
<th id="h-2025" colspan="2" scope="colgroup">2025</th>
</tr>
<tr>
<th id="h-2024-q1" scope="col">Q1</th>
<th id="h-2024-q2" scope="col">Q2</th>
<th id="h-2025-q1" scope="col">Q1</th>
<th id="h-2025-q2" scope="col">Q2</th>
</tr>
</thead>
<tbody>
<tr>
<th id="h-widgets" scope="row">Widgets</th>
<td headers="h-2024 h-2024-q1 h-widgets">150</td>
<td headers="h-2024 h-2024-q2 h-widgets">200</td>
<td headers="h-2025 h-2025-q1 h-widgets">180</td>
<td headers="h-2025 h-2025-q2 h-widgets">220</td>
</tr>
</tbody>
</table>Do not use tables for layout
This is a fundamental rule that is still violated today :
<!-- Do not — table used for layout -->
<table>
<tr>
<td>Sidebar content</td>
<td>Main content</td>
</tr>
</table>
<!-- Do — use CSS for layout -->
<div class="layout">
<aside>Sidebar content</aside>
<main>Main content</main>
</div>If you absolutely must use a table for layout (legacy systems), add
role="presentation"to strip all table semantics from screen readers.
Responsive tables
On narrow screens, data tables are challenging. Common patterns and their accessibility implications :
Horizontal scrolling
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-wrapper:focus-within {
outline: 2px solid #005fcc;
}<div class="table-wrapper" tabindex="0" role="region" aria-label="Quarterly revenue table">
<table>
<!-- table content -->
</table>
</div>Add
tabindex="0"androle="region"with anaria-labelon the scrollable wrapper so keyboard users can scroll and screen readers announce the scrollable area.
Stacked cards pattern
For mobile, transform table rows into stacked cards using CSS :
@media (max-width: 600px) {
table, thead, tbody, th, td, tr {
display: block;
}
thead {
position: absolute;
clip: rect(0 0 0 0);
}
td {
padding-left: 50%;
position: relative;
}
td::before {
content: attr(data-label);
position: absolute;
left: 0.5rem;
font-weight: 700;
}
}<tr>
<td data-label="Region">North America</td>
<td data-label="Q1">$320</td>
<td data-label="Q2">$410</td>
</tr>Scenarios and Edge Cases
Sortable table columns
When columns are sortable, the sort state must be communicated :
<th scope="col">
<button aria-sort="ascending">
Name
<span aria-hidden="true">▲</span>
</button>
</th>aria-sortvalues :ascending,descending,none,other.- Announce sort changes via an
aria-liveregion.
Empty cells
Empty table cells should still contain something for screen readers to navigate :
<!-- Do not leave completely empty -->
<td></td>
<!-- Better — indicate no data -->
<td>—</td>
<!-- Or use visually hidden text -->
<td><span class="sr-only">No data available</span>—</td>Tables with interactive content
Tables containing buttons, links, or form controls require extra care :
- Each interactive element must have a unique, descriptive accessible name.
- Ensure keyboard users can navigate both the table cells and the interactive elements within them.
<td>
<button aria-label="Edit record for North America">Edit</button>
<button aria-label="Delete record for North America">Delete</button>
</td>Resources
If you found this helpful, share it with someone who's building for the web.