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.
Who it affects
Section titled “Who it affects”- People who use a screen reader or a braille display, most of them blind.
How to fix it
Section titled “How to fix it”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.
Fixes that fail
Section titled “Fixes that fail”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.
In React
Section titled “In React”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.
Fixes that fail in React
Section titled “Fixes that fail in React”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.
More on the rule
Section titled “More on the rule”axe-core’s documentation of the rule
CORA’s fix guidance adapts content from accessibility-agents, under the MIT License: see its notice.