Broken table header association (td-headers-attr)

The td-headers-attr rule flags a table cell whose headers attribute refers to something that is not a header cell of the same table – a missing id, a typo, or a cell in another table. A screen reader then reads the value without the column and row it belongs to. The fix is to point headers at real <th> ids in the same table, or, for a simple table, to drop headers and use scope.

What the rule means

In a data table, each data cell belongs to one or more header cells. Most tables express that by structure alone: <th> in the first row and first column, ideally with scope="col" or scope="row". Complex tables – headers spanning several columns, several levels of headers – can name the relationship explicitly: each <td> gets a headers attribute listing the ids of its header cells.

The rule checks every headers attribute: each id must belong to a cell in the same table, and a cell must not list itself. It does not check whether the headers named are the right ones.

Who is affected

Blind and partially sighted people using a screen reader. When they move through a table cell by cell, the screen reader announces the headers of each cell – "Express, Price, €9.90". With a broken headers attribute that announcement is missing or wrong, and a table of numbers becomes a list of numbers with no meaning.

Why the check fails

  • Ids that changed – a template renamed the header ids and the cells kept the old ones.
  • Generated ids that differ between the header row and the body, for example a counter restarted per section.
  • Two tables on one page sharing ids, so cells point into the other table.
  • Copy-pasted rows from another table in a content editor.
  • headers used where scope would do, which multiplies the places an id can break.

How to fix it

Point every headers value at the id of a <th> in the same table.

<!-- Before: the cells point at ids that do not exist -->
<table>
  <caption>Delivery options</caption>
  <tr><th id="option">Option</th><th id="price">Price</th></tr>
  <tr><td headers="opt">Standard</td><td headers="cost">€4.90</td></tr>
  <tr><td headers="opt">Express</td><td headers="cost">€9.90</td></tr>
</table>
<!-- After: a simple table needs no headers attribute at all -->
<table>
  <caption>Delivery options</caption>
  <tr><th scope="col">Option</th><th scope="col">Price</th></tr>
  <tr><th scope="row">Standard</th><td>€4.90</td></tr>
  <tr><th scope="row">Express</th><td>€9.90</td></tr>
</table>

Keep headers for tables where scope cannot express the structure – for example a timetable with two header rows. Then generate the ids and the references from the same data, so they cannot drift apart.

How to test it manually

  1. In the developer tools, search the page for each id a headers attribute names. Does it exist, and is it a <th> in the same table?
  2. With a screen reader, move through the table cell by cell (in NVDA: Ctrl + Alt + arrow keys). Are the right headers announced for each cell?
  3. Check tables with merged header cells especially carefully – these are where headers is really needed.
  4. Ask whether the table needs headers at all. A table with one header row and one header column does not.

1.3.1 Info and Relationships, Level A. Related: 1.3.2 Meaningful Sequence, because a table read in order still has to make sense.

How Reviseberg reports it

Reviseberg runs td-headers-attr on every crawled page. The issue list shows the rule with its severity, WCAG 1.3.1, the number of affected cells and pages, and the points a fix gets you back; the detail view shows each cell's selector and HTML snippet. Tables built by the same template fail the same way, so the pages column usually points at the template to fix. Whether the headers a cell names are the correct ones is a judgement the rule cannot make – check that with a screen reader.

  • undefined – a list item outside a list.
  • undefined – list structure, the other common 1.3.1 structure.
  • undefined – a heading with no text.

Check the tables on your site – get a free scan

Frequently asked questions

Should I use headers or scope?

scope for simple tables with one row and one column of headers; headers only for complex tables that scope cannot describe.

Can headers point at a <td>?

The attribute may refer to any cell in the same table, and axe accepts that. In practice the referenced cells should be <th>, so every tool treats them as headers.

Does a layout table need headers?

No. A table used for layout should not have headers at all – better still, use CSS instead of a table.

Sources

  1. W3C, Understanding SC 1.3.1 Info and Relationships – https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships
  2. Deque University, axe-core 4.13 rule td-headers-attr – https://dequeuniversity.com/rules/axe/4.13/td-headers-attr
  3. W3C WAI, Tables Tutorial: Tables with multi-level headers – https://www.w3.org/WAI/tutorials/tables/multi-level/
  4. W3C, Technique H43: Using id and headers attributes to associate data cells with header cells – https://www.w3.org/WAI/WCAG22/Techniques/html/H43

See what a scan finds on your site

One page in about thirty seconds, no email needed. The full report covers up to 100 pages and a keyboard journey.