headers attribute must refer to cells in the same table
A data cell's headers attribute lists an id that doesn't exist or sits outside the table, so the cell-to-header association silently breaks.
What you see
All cells in a table using the headers attribute must refer to other cells in that table The headers attribute is not exclusively used to refer to other cells in the table <td headers="col3 hdr-total">…</td>
What’s actually happening
axe flags a <td> (or <th>) whose headers attribute points at an id that either doesn't exist anywhere or belongs to an element outside this table. headers is meant to bind a data cell to its header cells by id for complex tables, so a screen reader can announce "Revenue, Q3: $40k" instead of just "$40k". When an id is wrong, that announcement falls apart — the cell gets associated with nothing, or the parser ignores the whole headers attribute. Sighted users see a normal grid; the breakage is entirely in what AT reads out when navigating cell by cell.
Common causes
- A typo in an id reference: headers="col-totl" when the header cell is id="col-total"
- The referenced id exists, but on an element outside this table (a heading above it, a cell in a different table) — headers may only point within the same table
- A header cell was renamed or its id removed, but the data cells still reference the old id
- A table built in a loop emits headers values that don't line up with the generated th ids (off-by-one, wrong variable)
- Copy-pasted rows kept headers pointing at the first table's ids after the block was duplicated into a second table
How to fix it
- Make every referenced id a real th or td in the same tableFor each value in a headers attribute, there must be a matching id on a cell inside the same <table>. Audit them: in the console run document.querySelectorAll('[headers]').forEach(c=>c.getAttribute('headers').split(/\s+/).forEach(id=>{if(!c.closest('table').querySelector('#'+CSS.escape(id)))console.warn(c,'-> missing',id)})). Fix or remove each id it logs.
- Fix the typo or restore the missing header idIf the target just got misspelled or dropped, correct the id on the header cell (or the reference on the data cell) so the two strings match exactly. ids are case-sensitive and must be unique on the whole page, so a duplicate id elsewhere can also be the culprit.
- Consider dropping headers and using scope insteadheaders/id is only needed for irregular tables — merged cells, multiple header rows, split headers. For a plain grid with one header row and/or one header column, delete the headers attributes and put scope="col" / scope="row" on the <th> cells. The browser infers associations automatically and there are no ids to break.
- Generate ids and headers together in loopsWhen you render a table programmatically, build the th id and the matching td headers from the same source value (the column key, the row index) so they can't drift. Don't hardcode headers strings next to dynamically generated ids.
- Verify with a screen reader's table navigationAfter fixing, open the table in NVDA or VoiceOver and move with the table-navigation keys (Ctrl+Alt+arrows in NVDA). Each data cell should announce its associated row and column headers. If a cell reads its value with no header context, a reference is still wrong.
Stop it recurring
For simple tables use scope on header cells instead of headers/id; reserve the headers attribute for genuinely complex tables and generate its ids programmatically.