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.

© 2026 Xygeni. All rights reserved