# Non-empty <td> elements in larger <table> must have an associated table header

> Canonical HTML version: https://rocketvalidator.com/accessibility-validation/axe/4.11/td-has-header
> Attribution: Rocket Validator (https://rocketvalidator.com)
> License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

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.

```html
<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.

```html
<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.

```html
<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 `scope` for simple tables and `id` + `headers` for 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.
