About This Accessibility Rule
Data tables communicate structured information where each cell’s meaning depends on its position relative to row and column headers. When you look at a table visually, your eyes naturally track back to the headers to understand what a particular value represents. Screen readers replicate this behavior by announcing the relevant headers as users navigate between cells — but only when the headers are properly marked up in the HTML.
Without proper header associations, a screen reader might announce a cell value like “28:04” with no context. The user would have no way to know what that number represents, who it belongs to, or what category it falls under. This is a critical accessibility barrier for people who are blind or deafblind.
This rule relates to WCAG 2.1 Success Criterion 1.3.1 (Info and Relationships), which requires that information and relationships conveyed through visual presentation be programmatically determinable. It is also required by Section 508 guidelines, which mandate that row and column headers be identified for data tables and that markup be used to associate data cells with header cells for tables with two or more logical levels of headers.
The axe rule td-has-header checks tables that are at least 3 cells wide and 3 cells tall. If any non-empty <td> in such a table lacks an associated header, the rule fails.
How to Fix It
There are two primary techniques for associating data cells with headers:
Using <th> with scope
This is the simplest and preferred approach for straightforward tables. Replace header cells’ <td> elements with <th> elements and add a scope attribute:
-
Use
scope="col"for column headers. -
Use
scope="row"for row headers.
Screen readers use the scope attribute to determine which data cells belong to which header. This approach works well for tables where each column and row has a single header.
Using id and headers
For complex tables — such as those with merged cells, multiple header levels, or irregular structures — use the id and headers method. Give each <th> a unique id, then add a headers attribute to each <td> listing the id values of all headers that apply to that cell, separated by spaces.
This method is more verbose but allows you to explicitly define exactly which headers relate to each data cell, regardless of table complexity. Where possible, consider breaking a complex table into multiple simpler tables, which benefits all users.
Examples
Failing: Table with no headers
This table has no <th> elements at all, so screen readers cannot associate any data cell with a header.
<table>
<tr>
<td>Name</td>
<td>1 mile</td>
<td>5 km</td>
<td>10 km</td>
</tr>
<tr>
<td>Mary</td>
<td>8:32</td>
<td>28:04</td>
<td>1:01:16</td>
</tr>
<tr>
<td>Betsy</td>
<td>7:43</td>
<td>26:47</td>
<td>55:38</td>
</tr>
</table>
Passing: Simple table with scope
Column headers use scope="col" and row headers use scope="row", giving every <td> a clear association.
<table>
<caption>Greensprings Running Club Personal Bests</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">1 mile</th>
<th scope="col">5 km</th>
<th scope="col">10 km</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Mary</th>
<td>8:32</td>
<td>28:04</td>
<td>1:01:16</td>
</tr>
<tr>
<th scope="row">Betsy</th>
<td>7:43</td>
<td>26:47</td>
<td>55:38</td>
</tr>
<tr>
<th scope="row">Matt</th>
<td>7:55</td>
<td>27:29</td>
<td>57:04</td>
</tr>
</tbody>
</table>
When a screen reader user navigates to the cell containing “26:47”, the screen reader announces something like “Betsy, 5 km, 26:47” — providing full context.
Passing: Complex table with id and headers
When a table has multiple levels of headers or merged cells, the id and headers method gives you explicit control over associations.
<table>
<caption>Race Results by Category</caption>
<thead>
<tr>
<td></td>
<th id="road" colspan="2">Road Races</th>
<th id="track" colspan="2">Track Events</th>
</tr>
<tr>
<td></td>
<th id="r5k">5 km</th>
<th id="r10k">10 km</th>
<th id="t800">800 m</th>
<th id="t1500">1500 m</th>
</tr>
</thead>
<tbody>
<tr>
<th id="mary">Mary</th>
<td headers="mary road r5k">28:04</td>
<td headers="mary road r10k">1:01:16</td>
<td headers="mary track t800">2:34</td>
<td headers="mary track t1500">5:51</td>
</tr>
<tr>
<th id="betsy">Betsy</th>
<td headers="betsy road r5k">26:47</td>
<td headers="betsy road r10k">55:38</td>
<td headers="betsy track t800">2:17</td>
<td headers="betsy track t1500">5:09</td>
</tr>
</tbody>
</table>
In this example, when a screen reader lands on “26:47”, it can announce “Betsy, Road Races, 5 km, 26:47” because the headers attribute explicitly lists all three relevant header id values.
Key points to remember
-
Always use
<th>for header cells, not styled<td>elements. -
Add a
<caption>to give your table a descriptive name. -
Use
scopefor simple tables andid+headersfor complex ones. -
Empty
<td>cells are excluded from this rule — only non-empty data cells need header associations. - Visual styling (borders, fonts, backgrounds) should be handled with CSS and has no impact on accessibility markup.
Learn more:
Help us improve our guides
Detect accessibility issues automatically
Rocket Validator scans thousands of pages with Axe Core and the W3C Validator, finding accessibility issues across your entire site.