squirrelscan
GitHub

Table headers: why a data table needs th cells

A table built from styled td cells has no header relationships for a screen reader. squirrel checks every data table for th elements.

What a table header is

A <th> is what turns a grid of text into a table a screen reader can navigate. Moving between cells, it announces the column and row header along with the value, so a user hears “Revenue, Q3, 6.8M” instead of “6.8M”.

Styling a <td> to look like a header gives none of that. It looks identical and reads as another data cell.

What squirrel checks

One check, table-headers, at severity warning. It covers every <table> except those with role="presentation" or role="none".

  • Warn when a table contains no <th> at all. Report message: N table(s) without <th> headers. Each is listed as Table 1, numbered in document order across the tables the rule looked at, and every offender is listed.
  • Pass with All N data table(s) have headers.
  • Info with No data tables found.

The check tests presence only. It does not look at scope or at <caption>, and no rule in this category does. Whether each header has data under it is a11y/th-has-data-cells, and whether explicit headers references resolve is a11y/td-headers-attr.

How to fix it

<table>
  <caption>Revenue by quarter</caption>
  <thead>
    <tr>
      <th scope="col">Quarter</th>
      <th scope="col">Revenue</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Q3</th>
      <td>6.8M</td>
    </tr>
  </tbody>
</table>

Use <th> for both the column headings and the row labels, and add scope so the direction is unambiguous. Two lines of CSS restore whatever styling the <td> had.

A table used purely for layout should carry role="presentation", which removes it from this check and from the table list a screen reader builds.

Rule IDa11y/table-headers
CategoryAccessibility
ScopePer-page
Severitywarning
Weight4/10

Enable / disable

Disable this rule

[rules]
disable = ["a11y/table-headers"]

Disable all Accessibility rules

[rules]
disable = ["a11y/*"]

Enable only this rule

[rules]
enable = ["a11y/table-headers"]

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.

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. Table 3 means the third non-presentational table in document order. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.

Type to search…

↑↓ navigate↵ openesc close