Table Header Without Scope Or Id
ID |
html.th_id_or_scope |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Html |
Tags |
accessibility, reliability |
Description
Flags a cell of a data table whose header association cannot be resolved: a <th> that declares
neither scope nor id, and a <td> that declares scope, which is a header-cell attribute the
browser ignores there.
Rationale
A header cell only helps once a reader can tell which cells it heads. HTML offers two ways to say
it: scope on the header (col, row, colgroup, rowgroup), or an id on the header that data
cells name in their headers attribute. With neither, the association falls back to the browser’s
default - the first row is column headers, the first cell of a row is that row’s header.
That default is the right answer for a header in either of those two places, so a header there is
not reported: asking a simple table to spell out what the browser already resolves correctly is
noise, and it is the overwhelming majority of the <th> on any page. Only a header the default
cannot reach accuses — the second row of a two-level heading, a header in the middle of a row — and
one finding per table, on the first such header, because the fix is a single pass over the whole
table’s heading.
<table>
<tr><th scope="col">Name</th><th scope="col">Quarter</th></tr>
<tr><td>Ada</td><th>Q1</th></tr> <!-- FLAW - no scope, no id, out of reach -->
</table>
<table>
<tr><th scope="col">Name</th><th scope="col">Quarter</th></tr>
<tr><td>Ada</td><th scope="">Q1</th></tr> <!-- FLAW - blank is the undefined state -->
</table>
<table>
<tr><th scope="col">Name</th><th scope="col">Role</th></tr>
<tr><td scope="row">Ada</td><td>Owner</td></tr> <!-- FLAW - scope is th-only, does nothing here -->
</table>
<table>
<tr><th>Name</th><th>Role</th></tr> <!-- OK - the first row is the default's answer -->
<tr><th>Ada</th><td>Owner</td></tr> <!-- OK - so is the first cell of a row -->
<tr><td>Ada</td><th scope="row">Q1</th></tr> <!-- OK -->
<tr><td>Ada</td><th id="q1">Q1</th></tr> <!-- OK - data cells point back with headers -->
<tr><td>Ada</td><th th:scope="${axis}">Q1</th></tr> <!-- OK - computed, but declared -->
<tr><td th:scope="${axis}">Ada</td></tr> <!-- OK - a directive, not markup this file judges -->
</table>
Either spelling of the attribute answers for it: a th:scope directive does put a scope on the
rendered page, and an exemption that missed it would be a false positive. A value settled at render
time counts as declared for the same reason. What does not answer is a valueless or blank attribute -
that is the attribute’s undefined state, and the browser falls back exactly as if it were absent.
scope on a <td> is the mirror-image mistake, and gets its own message. The attribute is defined
on <th> only; on a data cell it does nothing whatever its value, and it is written when the cell
was meant to be a header in the first place. Only the plain spelling accuses - a th:scope
directive on a cell is the template library’s business - and the value is not read, since none of
them has any effect there.
Only the cells of a data table are judged. A <th>, or a stray scope, in a table declared
role="presentation" is a finding of html.layout_table_semantic_markup instead, so the two rules
never both report the same markup. The table shapes neither rule judges are the ones described in
html.table_headers - a layout wrapper, generated markup, or a role settled at render time.
One shape is worth naming as a limitation: a table whose rows are generated from a single source row
(<tr th:each="m : ${team}">, v-for, *ngFor) is not judged. The file shows one row of a table
that will render with many, so its shape here is not the shape a user meets - the same reason
generated markup is left alone throughout this family. Such a table’s headers are not checked by
this rule, by html.table_headers or by html.table_cell_headers.
Remediation
Add the scope the header covers:
<table>
<caption>The team</caption>
<tr><th scope="col">Name</th><th scope="col">Role</th></tr>
<tr><th scope="row">Ada</th><td>Owner</td></tr>
</table>
Use scope="col" for a header above its column, scope="row" for one at the start of its row, and
colgroup / rowgroup for a header spanning a group. When the layout is more involved than that,
give each header an id and let the data cells name them in headers - see
html.table_cell_headers.
For a <td scope="…">, decide what the cell is. If it heads its row or column, make it a <th>
and keep the scope - the row above is what that looks like; if it holds data, drop the attribute,
which has no effect there either way.