( Perceivable / WCAG 1.3.1 )

Table header has no associated data cells

SeriousLevel AWCAG 1.3.1 — Info and Relationships

What is this issue?

A <th> element is present in a table, but when a browser computes which data cells it applies to (using scope, an implicit row/column position, or an explicit headers/id reference elsewhere in the table), the answer comes back as zero. The header cell exists and has text, but nothing in the table actually points to it as a header.

This most often happens in irregular tables: a merged cell with colspan or rowspan shifts which column position a <th>’s implicit scope would otherwise apply to, or an explicit headers/id reference elsewhere in the table was never written to include this particular header’s id.

Why does this matter?

A <th> with no associated data cells is a header describing nothing. For a screen reader user navigating the table cell by cell, that header simply never gets announced with any content: it’s invisible to the exact navigation workflow it exists to support, even though it’s still visually present on the page for a sighted user to read directly.

Beyond the missing header itself, this failure is often a symptom of a table whose overall header structure isn’t reliable. A <th> with zero data cells frequently means the other headers in the same table have an association problem too (a merged header row where the column positions don’t line up the way the markup assumes, for example), so finding one unassociated header is a strong signal to check the whole table’s header structure, not just that one cell.

Who is affected?

  • Screen reader users: never hear the unassociated header announced as context for any data cell, even though the header is visually present and a sighted user reads it normally.
  • Cognitive disabilities: users relying on consistent header announcements across every cell in a table encounter gaps that make the table’s overall structure feel unreliable, adding uncertainty to reading any of its data.

What users experience

Tomás uses JAWS on Windows to review a staffing table with a merged “Team” header spanning two columns above separate “Lead” and “Members” sub-headers. The merged header cell uses scope="col", which JAWS interprets as applying only to the single column position directly beneath it (not both columns the merge visually spans), leaving the “Members” column’s data cells with no header association at all. As JAWS moves through that column, it reads names with no header context whatsoever, while the “Lead” column right next to it announces cleanly, so Tomás has no way to tell from the announcement alone that the unlabeled names are team members rather than something else entirely.

How do I fix it?

Correct the association so every <th> genuinely applies to at least one real data cell: set scope="col" or scope="row" accurately for a simple table, or fix the headers/id pairing so every relevant <td> lists the right header ids for a complex one. This works because it restores the actual lookup a screen reader performs: once the association exists, the header text gets included in every relevant cell’s announcement again.

For a table with merged or multi-level headers (the case most likely to produce this failure), scope alone usually isn’t precise enough, since it only expresses a simple single-column or single-row relationship. Switch to explicit headers/id for that table: give every header cell a unique id, and list every relevant header’s id in each data cell’s headers attribute, which lets you express exactly which headers apply to a cell regardless of how the visual layout is merged or spans multiple columns.

Code Examples

Before
<table>
  <tr>
    <th id="name">Product</th>
    <th id="status">Status</th>
    <th id="region">Region</th>
  </tr>
  <tr>
    <td headers="name">Widget A</td>
    <td headers="status">Active</td>
    <td>EMEA</td>
  </tr>
</table>
After
<table>
  <tr>
    <th id="name">Product</th>
    <th id="status">Status</th>
    <th id="region">Region</th>
  </tr>
  <tr>
    <td headers="name">Widget A</td>
    <td headers="status">Active</td>
    <td headers="region">EMEA</td>
  </tr>
</table>

The id="region" header was already present and correctly written; the missing piece was the headers="region" attribute on the data cell underneath it. Adding that one attribute completes the association, so the “Region” header now applies to the “EMEA” value the way “Product” already applies to “Widget A.”

Common Mistakes

Mistake: “I added scope to every th, so every header must be associated correctly now.” scope="col" or scope="row" only expresses a simple, single-column or single-row relationship; it doesn’t handle merged or multi-level headers correctly, and a header positioned above a colspanned group of cells can end up with scope applying to only one of those columns, leaving the rest unassociated. Check merged-header tables specifically, since scope alone often isn’t enough for them.

Mistake: “This header describes the whole table, so it doesn’t need to be associated with individual cells.” A <th> meant to label the entire table isn’t the right use of the element; that’s what <caption> is for. Every <th> inside the table body is expected to apply to specific data cells; if a piece of text applies to the whole table rather than a row or column, it belongs in a <caption>, not as an unassociated <th>.

Mistake: “The header row renders fine and looks aligned, so the association underneath it must be correct too.” Visual column alignment and the underlying scope/headers association are computed completely separately: a <th> can render in the visually correct position while its programmatic association points nowhere, or nowhere close to where it visually sits. Only inspecting the markup, or testing with a screen reader, reveals the mismatch.

How RedFlag Detects This

Automated: axe-core rule, runs on every scan. RedFlag calls axe-core’s th-has-data-cells rule as part of every scan, restricted to the WCAG 2.0/2.1/2.2 A and AA rule set. The rule computes each <th>’s effective association (through scope, implicit row/column position, or headers/id) and flags any header that resolves to zero data cells.

False negative: axe-core confirms at least one data cell resolves to each header; it can’t confirm every cell that should be associated actually is. A header correctly linked to one data cell out of five it should apply to passes this check, even though four cells in that column or row are still missing that header’s context. False positive: none typical for this specific check, since computing whether zero cells resolve to a header is a binary structural condition axe-core evaluates reliably. Manual step: for any complex or merged-header table, sample several cells across the full width and depth of the table with a screen reader and confirm every one announces the header context you’d expect, not just the cells nearest the header row.

Manual Testing

  1. Open the page in Chrome or Firefox with NVDA or JAWS running on Windows.
  2. Navigate into the table using column-navigation commands (NVDA/JAWS: Ctrl+Alt+Right/Left Arrow to move between columns, Ctrl+Alt+Down/Up Arrow to move between rows).
  3. For each column, listen to whether the column header is announced before the cell value as you move down it.
  4. Pay particular attention to any column beneath a merged or multi-column header: this is where an association gap is most likely to appear.
  5. If any cell in a column announces its value with no header context while the rest of that column announces correctly, the header above it likely has an association problem.

1.3.1 Info and Relationships: Information, structure, and relationships conveyed through presentation must also be available programmatically. A header cell that’s visually understood to apply to a column or row, but has no working programmatic association to any cell in it, is a direct failure of this criterion.

Table cell headers attribute references a missing id is the reverse direction of the same broken-association family: instead of a <th> with no <td> pointing back to it, it’s a <td> pointing at a <th> id that doesn’t exist at all.

Table header scope value is invalid is a common direct cause of this failure in simpler tables: an invalid scope value can result in the browser computing no valid association for that header.

Table is missing header cells is the more basic failure underneath this whole family: a table with no <th> elements at all, which is a different and more severe problem than a <th> that exists but isn’t associated correctly.

Complex data table is missing a caption tends to appear on the same kind of tables as this rule, since both are most consequential on genuinely complex, multi-level tables rather than simple ones.

References

Frequently asked questions

Can a th genuinely have zero data cells on purpose, for a decorative spacer column?

If a column or row is purely decorative with no data, it should not use th at all, since th specifically declares a header for real data cells. Use an empty td, or restructure the table, rather than an unassociated th.

Is this the same failure as a table having no th elements at all?

No. This rule assumes at least one th exists but is not correctly linked to any td; a separate, more basic rule covers a table that has no th elements anywhere in it.

Does adding scope="col" to every th in the header row automatically fix this?

In most simple tables, yes, since scope="col" on a header row cell correctly associates it with every data cell in that column. It will not fix a th that is meant to represent a row header, or one inside a complex table relying on headers/id instead of scope.

Why would a table pass every other table-related check but still fail this one?

Because this check is specifically about the th to td association, not whether th elements exist or whether the table has a caption. A table can have well-written headers and a caption and still fail here if one specific th, often in a merged or irregular row, ends up with no data cell actually pointing back to it.

Does colspan or rowspan on a data cell affect this check?

Yes, indirectly. A merged cell spanning multiple columns can shift which th a given column position implicitly associates with, and getting that association wrong is a common cause of a th ending up with no data cells correctly linked to it in an irregular table.