For each data table, is the appropriate technique used to associate each cell with its headers (except in special cases)?

A screen reader navigating a data table announces the active cell, then its associated headers. If the technical association is missing, the user hears "15%" with no context: they don't know if it's a success rate, a discount, or an exam score. Without association, there is no context.

Two situations to distinguish. For a simple table with a single row or single column of headers, <th> tags without additional attributes are sufficient: the screen reader deduces associations from position in the table. For more complex tables, associations must be declared explicitly via scope, via id/headers, or via ARIA roles.

For complex tables, two techniques coexist. scope="col" on column headers and scope="row" on row headers cover the majority of common cases. When a header applies only to part of a row or column, only the id/headers technique works: each <th> receives a unique id, and each data cell lists the ids of its headers in a headers attribute separated by spaces. This is the most powerful technique, but also the most prone to maintenance errors.

5 tests to check the link between cells and table headers

id, scope or ARIA role attribute on complete <th>

For each data table on the page:

  1. Identify all <th> elements that span an entire row or column.
  2. For each, verify that it has at least one of these attributes:
    • a unique id attribute,
    • a scope attribute,
    • or role="rowheader" / role="columnheader".
  3. If a <th> spanning an entire row or column lacks all three, the test fails.

Exception: for tables whose headers are on a single row or single column, <th> elements without attributes are accepted.

Correct value of scope attribute on <th>

For each <th> spanning an entire row or column and bearing a scope attribute:

  1. Verify that scope equals "row" if it is a row header.
  2. Verify that scope equals "col" if it is a column header.
  3. Any other value (for example scope="rowgroup" misused on a simple <th>) causes the test to fail.

Unique id attribute on partial <th>

For each <th> that does not span an entire row or entire column (partial header):

  1. Verify that it has a unique id attribute.
  2. Verify that it does not bear a scope attribute.
  3. Verify that it does not bear role="rowheader" or role="columnheader".
  4. If any of these three conditions is not met, the test fails. The only valid technique for these partial headers is id + headers on the associated cells.

headers attribute on cells linked to <th> with id

For each <td> or <th> whose headers bear id attributes:

  1. Verify that the cell has a headers attribute.
  2. Verify that this attribute lists exactly all the ids of the headers that apply to this cell (values separated by spaces).
  3. A missing, incorrect, or wrongly ordered id causes the test to fail. Verify each value individually after any copy-paste of rows.

Correct ARIA role on table headers

For each header spanning an entire row or column and bearing role="rowheader" or role="columnheader":

  1. Verify that role="rowheader" is used for row headers.
  2. Verify that role="columnheader" is used for column headers.
  3. A reversed role (rowheader on a column header, or vice versa) causes the test to fail.

Examples

❌ Non-compliant : Complex table with incorrect headers attributes

<table>
  <caption>Results by subject and semester</caption>
  <tr>
    <td></td>
    <th id="s1">Semester 1</th>
    <th id="s2">Semester 2</th>
  </tr>
  <tr>
    <th id="maths">Mathematics</th>
    <td headers="maths s2">14/20</td>
    <td headers="maths s1">11/20</td>
  </tr>
  <tr>
    <th id="fr">French</th>
    <td headers="fr s2">16/20</td>
    <td headers="fr s1">13/20</td>
  </tr>
</table>

The headers attributes reference the wrong ids: the score "14/20" is associated with "Semester 2" when it belongs to Semester 1. JAWS will announce "Mathematics Semester 2 14/20", which is incorrect. This error occurs systematically when copy-pasting rows without updating references.

✅ Compliant : Table with scope correctly set

<table>
  <caption>Results by subject and semester</caption>
  <thead>
    <tr>
      <td></td>
      <th scope="col">Semester 1</th>
      <th scope="col">Semester 2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Mathematics</th>
      <td>14/20</td>
      <td>11/20</td>
    </tr>
    <tr>
      <th scope="row">French</th>
      <td>16/20</td>
      <td>13/20</td>
    </tr>
  </tbody>
</table>

Each column <th> bears scope="col", each row <th> bears scope="row". The screen reader correctly announces "Mathematics Semester 1 14/20" for each cell. No id or headers attribute is necessary for this level of complexity.

✅ Compliant : Multi-level table with id and headers

<table>
  <caption>Detailed results by evaluation type</caption>
  <thead>
    <tr>
      <td></td>
      <th id="exam" colspan="2">Exams</th>
      <th id="proj" colspan="2">Projects</th>
    </tr>
    <tr>
      <td></td>
      <th id="e1" headers="exam">Midterm</th>
      <th id="ef" headers="exam">Final</th>
      <th id="p1" headers="proj">Submission 1</th>
      <th id="pf" headers="proj">Submission 2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th id="m">Math</th>
      <td headers="m e1">12/20</td>
      <td headers="m ef">14/20</td>
      <td headers="m p1">15/20</td>
      <td headers="m pf">17/20</td>
    </tr>
  </tbody>
</table>

With two levels of nested column headers, scope is no longer sufficient. The id/headers technique allows each cell to reference exactly its headers. JAWS announces "Math Exams Midterm 12/20", providing full context to the user.

Tips and pitfalls

⚠️ Simple tables: <th> elements without attributes are sufficient

When a table has headers on a single row or single column, <th> tags without scope, without id, and without ARIA role are accepted. The screen reader deduces associations from position in the grid. RGAA explicitly recognizes this special case: do not add unnecessary attributes to a simple table; you will not improve accessibility, you will complicate maintenance.

⚠️ Copy-pasting headers without reviewing them: the F90 error

The F90 failure (wrong id/headers association) is the most frequent error in complex table audits. A copy-pasted row retains old headers values, which reference the wrong headers. The visual rendering is correct; only a screen reader reveals the problem. After any copy-pasting of cells, review each headers attribute value one by one.

⚠️ Putting scope on a partial header

Test 5.7.3 forbids it: a <th> that does not span an entire row or column must not have scope. A scope="col" on a <th> that covers only three of the table's five rows misleads the screen reader, which applies this header to cells that do not correspond to it. The only valid technique for partial headers remains id + headers.

💡 scope covers 90% of common data tables

The id/headers technique is more powerful but heavier to maintain. For the majority of tables with a row of column titles and a column of row titles, scope="col" and scope="row" are sufficient, more readable in the code, and generate fewer errors during modifications. Reserve id/headers for tables with nested or partial headers.

⚠️ ARIA role="columnheader" on a <td>: valid but risky

Technically compliant per test 5.7.5, using role="columnheader" on a <td> in a native HTML table produces inconsistent results depending on browser/screen reader combinations. Reserve this technique for tables built with <div> and role="grid", where the <th> semantic element is unavailable. In a standard HTML <table>, <th scope="col"> remains the most reliable solution.

Frequently asked questions

What is the difference between scope and id/headers for associating table cells in RGAA?

scope for tables where each header covers an entire row or column. id/headers as soon as a header covers only part of the row or column, or when headers are on multiple nested levels. In practice: if your table has colspan or rowspan on <th> elements, think id/headers.

Why is a <th> without an association attribute non-compliant per RGAA?

Not systematically. RGAA provides an exception: for tables whose headers are on a single row or single column, <th> elements without attributes are accepted. A typical statistical table with a title row in the first row passes test 5.7.1 without any scope.

How to efficiently audit RGAA criterion 5.7 on cell/header association?

Enable NVDA with Firefox or JAWS with Chrome and navigate through the table cell by cell (arrow keys in table mode). For each data cell, the screen reader must announce relevant headers in the correct order. Also inspect the DOM: each id of a <th> must be referenced in exactly the right cells via headers, and no value must be duplicated or missing.

When and how to use scope="rowgroup" and scope="colgroup" per RGAA?

Test 5.7.2 lists only "row" and "col" as valid values for headers covering an entire row or column. rowgroup and colgroup are not covered by RGAA tests and their support by screen readers remains uneven across combinations. Avoid them: for structures requiring group association, use id/headers.

What is the RGAA rule for a partial <th> combining both id and scope?

No. Test 5.7.3 explicitly forbids it: a partial header must have a unique id, but must not have scope or role="rowheader"/"columnheader". The presence of scope on a partial header misleads the screen reader about this header's scope. Correct association passes only through the headers attribute of the relevant cells.

References