th cells with no data cells: a layout table in disguise
A header with no data in its row or column usually means the table is really a layout. squirrel builds the grid and checks each one.
What an orphaned header is
A <th> describes the cells in its row or its column. A header with no <td> anywhere in either direction describes nothing, and a screen reader announces a header relationship that leads nowhere.
Almost always this means the table is being used for layout. The <th> came from a template that expected data, and the cells around it are all headers or all empty.
What squirrel checks
One check, th-has-data-cells, at severity warning. For each table that is not role="presentation" or role="none", it builds a two-dimensional grid, expanding colspan and rowspan so every grid slot maps to the cell that occupies it. Both span attributes are read case-insensitively.
For each <th> in that grid it looks for a <td> in the same row, then in the same column.
- Warn when neither is found. Report message:
N <th> element(s) without associated data cells. Items quote the header’s first 30 characters, or readrow 2, col 3when the header has no text. Up to ten are shown with a count of the rest, and a header that spans several slots is reported once. - Pass with
All table headers have associated data cells. - Info with
No data tables found.
Building the grid is what makes the check work on tables with merged cells, but the test runs per grid slot. A header spanning two columns whose own row has no data cell is judged column by column, so data under one of the two columns does not clear the other.
How to fix it
For a table that is really a layout, say so:
<table role="presentation">
...
</table>That removes it from this check, from a11y/table-headers and from a11y/table-duplicate-name, which all skip presentational tables. a11y/td-headers-attr looks at every table regardless. Better still, replace it with CSS grid or flexbox, which is what the layout wanted in the first place.
For a real data table, check that each header has cells under or beside it. A header row followed by nothing, left in place while the data loads, produces this finding on the served HTML.
| Rule ID | a11y/th-has-data-cells |
| Category | Accessibility |
| Scope | Per-page |
| Severity | warning |
| Weight | 4/10 |
Enable / disable
Disable this rule
[rules]
disable = ["a11y/th-has-data-cells"]Disable all Accessibility rules
[rules]
disable = ["a11y/*"]Enable only this rule
[rules]
enable = ["a11y/th-has-data-cells"]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: data tables with no
<th>at all. - a11y/td-headers-attr: explicit associations pointing at ids that do not exist.
- a11y/table-duplicate-name: unique accessible names for the tables that survive this check.
- a11y/aria-required-parent: rows and cells sitting outside the structure they need.
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
Check your site
Run squirrel audit https://example.com and open the Accessibility section of the report. Each orphaned header is quoted by its text, or located by row and column. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.