empty-table-header

Table header cells must not be empty

manualconfidence: mediumseverity: minorperceivable

Checks that table header cells (<th>, or any element with role="columnheader"/"rowheader") have visible text content. A header named only via aria-label/aria-labelledby is also flagged, since real screen-reader/browser support for that is inconsistent.

Applies to. Applies to <th> elements that don't carry a conflicting explicit role, plus any element (native <th> or not) whose role resolves to columnheader or rowheader. The role attribute is a fallback list: its first known, non-abstract token, in any case, is the role, so role="foo columnheader" counts, and a <th role="foo"> (no known token) keeps its implicit header role. A <th> that explicitly restates role="columnheader"/"rowheader" is still covered, but a <th role="presentation"> (no longer meaningfully a header) is correctly excluded, and an ARIA-role-only header (e.g. a <div role="columnheader"> in a role="grid"/role="table" widget) is caught too. A custom element whose ElementInternals give it the columnheader or rowheader role (this.attachInternals().role), with no role attribute naming another, counts as a native element with that role does.

Expectation. The header cell has visible text content. A <th> named only via aria-label/aria-labelledby (no visible text) is ALSO flagged, not treated as equivalent: aria-label support on <th> is inconsistent in practice. NVDA+Firefox and iOS VoiceOver+Safari ignore it entirely (only visible text is announced), JAWS+Chrome/IE11 also only announce visible text in the header cell itself. Visible text is the one mechanism confirmed to work across every tested combination. See https://html5accessibility.com/stuff/2024/05/22/not-so-short-note-on-aria-label-usage-big-table-edition/.

What a finding reports.

  • ariaName (TABLE_HEADER_NAME_NOT_VISIBLE_TEXT): the name the header cell gets from aria-label or aria-labelledby instead of visible text.

These data.details fields help to read and reproduce a finding. Apart from reasonCode and the few fields Output schema names as stable, they are not a stable contract: a field may be renamed or dropped in a minor release, so don’t build on them.

Examples

pass
<table><tr><th>Name</th></tr></table>

The header cell has visible text content.

can't tell
<table><tr><th></th></tr></table>

The header cell has no text content and no accessible name.

How to check

When this rule can’t decide, it reports cantTell and leaves the question to a person. Each question it can ask is listed here under its reason code, with the steps to answer it. A finding carries the same steps as howToCheck, in the scan’s language. See Output schema.

TABLE_HEADER_EMPTY

  1. Find the header cell the report names and look at what it shows on screen.
  2. Inspect it in the Accessibility pane of the browser's developer tools and note its computed name; an image with alt text or a labelled icon inside can give it one.
  3. Look at the cells under or beside it and decide whether they need a header to be understood.
  4. If it is the empty top-left corner cell of a table whose first column holds row headers, note that it heads nothing.

Pass. The cell has a name that describes its column or row, or it is an empty corner cell that heads no data (better still marked up as a td).

Fail. The cell heads data cells but has no visible text and no name, so its column or row has no header, a best-practice miss for table header cells.

TABLE_HEADER_NAME_NOT_VISIBLE_TEXT

  1. Find the header cell the report names and read the name the report gives (from aria-label or aria-labelledby).
  2. Look at the cell on screen and confirm it shows no text of its own.
  3. With a screen reader, move through a data cell in that column or row and listen to whether the header name is announced with it.

Pass. The header is announced with its data cells, or the cell is given visible text (visually hidden text inside the cell also works) that states the same name.

Fail. The header has no visible text and relies on an ARIA name that is not announced with its data cells, a best-practice miss under accessible-name guidance.

View source