td headers attribute pointing at a missing th id
A headers attribute naming an id that does not exist leaves the cell unassociated. squirrel checks every reference in every table.
What the headers attribute is
In a complex table, headers on a <td> names the ids of the <th> cells that describe it. A screen reader reads those headers before the value, which is how a cell in a multi-level table announces “Revenue, Europe, Q3, 6.8M”.
Simple tables do not need it. One row of column headers and one column of row headers is fully described by scope="col" and scope="row". headers is for tables with split or stacked headers, where scope cannot express the relationship.
What squirrel checks
One check, td-headers-attr, at severity error and weight 6. For each <table> it collects the ids of every th[id] in that table, then reads every td[headers] and splits the value on whitespace.
- Fail when a referenced id does not belong to a
<th>in the same table. Report message:N invalid header reference(s) in table cells. Items readtd references non-existent header "q3" (content: "6.8M..."), quoting the first 15 characters of the cell. Up to ten are shown with a count of the rest. - Pass with
All td headers attributes reference valid th ids. - Info with
No td elements with headers attribute found, orNo tables found.
References are scoped per table, so a headers value pointing at a <th> in a different table is reported even though the id exists on the page.
How to fix it
<table>
<tr>
<td></td>
<th id="q3" scope="col">Q3</th>
</tr>
<tr>
<th id="eu" scope="row">Europe</th>
<td headers="eu q3">6.8M</td>
</tr>
</table>Every id in the headers value has to exist on a <th> in the same table. Ids that were renamed during a refactor are the usual cause, and so are ids generated per row that do not match the ones generated per header.
If the table is simple enough for scope alone, drop the headers attributes entirely. Fewer references means fewer to keep in sync.
| Rule ID | a11y/td-headers-attr |
| Category | Accessibility |
| Scope | Per-page |
| Severity | error |
| Weight | 6/10 |
Enable / disable
Disable this rule
[rules]
disable = ["a11y/td-headers-attr"]Disable all Accessibility rules
[rules]
disable = ["a11y/*"]Enable only this rule
[rules]
enable = ["a11y/td-headers-attr"]enable replaces the default ["*"], so this is the only rule selected. Leave disable out: a rule that matches any disable pattern is skipped even when enable names it. Only an enabled setting under a rule’s own [rule_options] table overrides both lists.
Related rules
- a11y/table-headers: whether the table has
<th>cells to point at. - a11y/th-has-data-cells: headers with no data in their row or column.
- a11y/duplicate-id-active: duplicated ids on focusable elements. It does not cover
<th>ids, so header collisions have to be caught by hand. - a11y/table-duplicate-name: telling several tables apart.
Accessibility findings ship in every audit next to the SEO, performance and agent experience rules. See Website security scan with AI for how an agent works through a report.
References
- The headers attribute, HTML Standard
- Tables with multi-level headers, W3C WAI tutorials
- The td element, MDN
Check your site
Run squirrel audit https://example.com and open the Accessibility section of the report. Each broken reference is listed with the id it names and the cell’s opening text. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.