Skip to content

Cells need to reference real header cells

  • Rule: axe/td-headers-attr
  • Checks: Table cell headers attributes must refer to other <th> elements in the same table
  • WCAG: 1.3.1
  • Fix class: mechanical: A fix needs no authored text or visual decision.
  • People who use a screen reader or a braille display, most of them blind.

headers lists an id that is not in the same table

Section titled “headers lists an id that is not in the same table”

Make each id in headers match the id of a <th> in the same table. Check for typos, and for ids that changed when the table was copied or generated.

<table>
<caption>Fruit prices</caption>
<tr>
<td rowspan="2"></td>
<th id="imported" colspan="2">Imported</th>
</tr>
<tr>
<th id="imported-apples">Apples</th>
<th id="imported-pears">Pears</th>
</tr>
<tr>
<th id="retail">Retail</th>
<td headers="imported imported-apples retail">$2.00</td>
<td headers="imported imported-pears retail">$2.50</td>
</tr>
</table>

headers points to a <td>, or a cell points to itself

Section titled “headers points to a <td>, or a cell points to itself”

Point headers only at header cells. If the referenced cell names a row or column, make it a <th>.

Every cell has just one column header and one row header

Section titled “Every cell has just one column header and one row header”

The table does not need headers. Remove the attribute and use <th> with scope="col" and scope="row", which is simpler to keep correct.

Removing headers from a table whose headers span several levels

Section titled “Removing headers from a table whose headers span several levels”

The rule passes, but scope alone may not tell screen readers which of the stacked headers apply to a cell. Fix the ids instead.

The part of the fix that differs in React, in its syntax. The rest of the guidance holds.

A table component hard-codes the id values its cells name in headers

Section titled “A table component hard-codes the id values its cells name in headers”

Prefix every header’s id with a value from useId, and build each cell’s headers from the same prefix and the row and column keys, so two tables in one React root never share ids. If the page holds more than one root, give each its own identifierPrefix, the same on the server and the client.

function PriceTable({ columns, rows }) {
const id = useId();
return (
<table>
<caption>Fruit prices</caption>
<thead>
<tr>
<td></td>
{columns.map((column) => (
<th key={column.key} id={`${id}-col-${column.key}`}>
{column.label}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.key}>
<th id={`${id}-row-${row.key}`}>{row.label}</th>
{columns.map((column) => (
<td key={column.key} headers={`${id}-col-${column.key} ${id}-row-${row.key}`}>
{row.values[column.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}

Header ids come from data that can contain spaces, such as a product name

Section titled “Header ids come from data that can contain spaces, such as a product name”

headers is a space-separated list, so an id with a space breaks into two references. Build ids from a stable key without spaces, such as a record id.

Generating ids with Math.random() or a module-level counter

Section titled “Generating ids with Math.random() or a module-level counter”

The ids differ between the server and the client render, which breaks hydration. useId gives the same id in both.

axe-core’s documentation of the rule

CORA’s fix guidance adapts content from accessibility-agents, under the MIT License: see its notice.