Table Cells Without Header References
ID |
html.table_cell_headers |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Html |
Tags |
accessibility, reliability |
Description
Flags a data cell that names no header, in a table whose header layout a browser cannot resolve on its own.
Rationale
A browser associates cells with headers by a fixed rule: the first row holds the column headers, and the first cell of a row holds that row’s header. That covers every simple table and nothing beyond it. Once a header sits anywhere else - the second row of a two-level heading, a header in the middle of a row - the association becomes guesswork, and a screen reader announces the values under the wrong names, which is worse than announcing them under none.
<table>
<tr><th id="region">Region</th><th colspan="2">Sales</th></tr>
<tr><td></td><th>Q1</th><th>Q2</th></tr>
<tr><th>North</th><td>10</td><td>20</td></tr> <!-- FLAW - which header heads 10? -->
</table>
<table> <!-- OK - a simple table resolves itself -->
<tr><th scope="col">Name</th><th scope="col">Role</th></tr>
<tr><td>Ada</td><td>Owner</td></tr>
</table>
<table>
<tr><th id="region">Region</th><th id="q1">Q1</th></tr>
<tr><td headers="region">North</td><th id="mid">Mid year</th></tr>
<tr><td headers="region q1">10</td><td headers="mid">5</td></tr> <!-- OK - written down -->
</table>
HTML’s answer is to write the association down: an id on each header cell, and a headers
attribute on each data cell listing the ids that apply to it. The rule asks for it only where the
default cannot reach - a header cell that is neither in the first row of the table nor the first cell
of its row - so a simple table, a table with a header row, and a table with a header column never
have to carry it.
There is one finding per table, on the first data cell that names no header: the fix is one pass over
the whole table, and repeating it per cell would bury it. A headers attribute that is valueless or
blank counts as naming none, since that is HTML’s undefined state; a value settled at render time
counts as a reference, since it does reach the page. Empty cells are skipped - they hold no value for
a header to name - and so are layout tables and the generated markup described in
html.table_headers.
The same limitation applies as to html.table_headers: a table whose rows are generated from a
single source row (<tr th:each="m : ${team}">, v-for, *ngFor) is not judged, because the file
shows one row of a table that will render with many, and an irregular header layout cannot be read
off that.
Remediation
Give every header cell an id, and list in each data cell the ids of the headers that apply to it:
<table>
<tr><th id="region" rowspan="2">Region</th><th id="sales" colspan="2">Sales</th></tr>
<tr><th id="q1" headers="sales">Q1</th><th id="q2" headers="sales">Q2</th></tr>
<tr>
<th id="north" headers="region">North</th>
<td headers="north q1 sales">10</td>
<td headers="north q2 sales">20</td>
</tr>
</table>
Splitting the table into two simple tables, each with a single header row, is often the better fix: a table that needs explicit references is usually one a sighted reader also struggles with.