Declare The Scope Of A JSX Table Header
ID |
javascript.jsx_table_cell_headers |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
JavaScript |
Tags |
accessibility, jsx, reliability |
Description
Reports a table cell 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 is left to the browser’s heuristic, which reads the first row as column headers and gives up on anything more structured. A table with both row and column headers, or with a header in the middle, is then announced wrongly — and the markup gives no sign of it.
scope on a <td> is the mirror-image mistake. It is defined on <th> only; on a data cell it
does nothing, and it is written when the cell was meant to be a header in the first place.
What the rule needs is not the table but the cell’s finished attributes. Three shapes give them: a
<table> encloses the cell in this file; the fragment the cell belongs to is itself a row or a row
group — a header row built into a local and spliced into a table further down
(const header = <tr><th>Editor</th></tr>) is one of the commonest React spellings, and a <tr>,
<thead>, <tbody> or <tfoot> has no legal parent but a table; or the cell is the whole
fragment, returned from a render helper or a component (return <th style={headerStyle}>{title}</th>).
That last one reads as an exception, and is not. Attributes reach a cell from outside only through a
spread, and a cell that spreads is already left alone. Without one, what is written here is all the
cell will ever carry, so wherever it is placed the association is missing — and <th> is legal
nowhere but a table. A cell under some other element (<div><th/>) is left alone: the markup around
it is already wrong in a way this rule has nothing to say about.
Any declared value satisfies the rule, computed ones included.
<table><tr><th>Name</th></tr></table> // FLAW - no scope, no id
<table><tr><td scope="row">Ada</td></tr></table> // FLAW - scope is th-only
<table><tr><th scope="col">Name</th></tr></table> // OK
<table><tr><th id="name">Name</th></tr></table> // OK - cells point back with headers
<table><tr><th scope={axis}>Name</th></tr></table> // OK - computed, but declared
return <th>{title}</th>; // FLAW - nothing can add scope later
<th {...props}>Name</th> // OK - the spread may carry scope
Remediation
Add scope="col" to a header that names a column and scope="row" to one that names a row; use
colgroup and rowgroup for a header spanning a group. For a table too irregular for scope, give
each header an id and list the ids a data cell belongs to in its headers attribute. On a <td>
that carries scope, decide which the cell is: make it a <th> if it names other cells, and drop
the attribute if it does not.