For each data table, is each column header and each row header correctly declared?
A screen reader user navigates a table cell by cell. With each move, they hear the cell's value — and, if headers are properly structured, its context. Without <th>, the reader announces "42". With a declared header, it announces "Sales, Quarter 2: 42". The data is there, but silent.
For assistive technologies to recognize a header, it must be declared programmatically. For headers covering an entire column or row, you have two options: a <th> element or a WAI-ARIA role="columnheader" / role="rowheader" attribute. For partial headers — those covering only part of a row or column — only <th> is accepted by RGAA.
This criterion verifies only header declaration. Explicit association between headers and data cells in complex tables is the subject of criterion 5.7.
4 tests to verify the presence of headers in data tables
Column headers via <th> or role="columnheader"
- Identify all data tables in the page (exclude layout tables).
- For each cell that serves as a header over an entire column, inspect its markup.
- Verify that it is:
- a
<th>element, or - any element bearing the
role="columnheader"attribute.
- a
- A single column header coded with
<td>is enough to fail the test. If all full-column headers meet the condition, the test is passed.
Row headers via <th> or role="rowheader"
- Identify all data tables in the page.
- Identify row headers: typically the first cell of each row, which names the category (region, product, person, etc.).
- For each header covering an entire row, verify that it uses:
- a
<th>element, or - an element bearing the
role="rowheader"attribute.
- a
- If all full-row headers meet this condition, the test is passed.
Partial headers via <th> element
- Identify all data tables in the page.
- Identify partial headers: those covering only part of a row or column (common in tables with grouped or merged headers across multiple levels).
- For each one, verify that it uses a
<th>element. - Caution:
role="columnheader"androle="rowheader"are not valid here. Only<th>is accepted for partial headers. - If all partial headers are
<th>, the test is passed.
Multi-header cells via <td> or <th>
- Identify all data tables in the page.
- Identify cells associated with multiple headers simultaneously (for example, a data cell at the intersection of a row header and multiple nested column headers).
- Verify that each of these cells is coded with a standard
<td>or<th>tag. - If all these cells use
<td>or<th>, the test is passed. In practice, if your table is in native HTML, this test is nearly automatically satisfied.
Examples
❌ Non-compliant : Visual headers without HTML semantics
<table>
<tr>
<td><strong>Product</strong></td>
<td><strong>Price excl. tax</strong></td>
<td><strong>Stock</strong></td>
</tr>
<tr>
<td>Mechanical keyboard</td>
<td>79 €</td>
<td>14</td>
</tr>
<tr>
<td>Ergonomic mouse</td>
<td>45 €</td>
<td>32</td>
</tr>
</table>The cells "Product", "Price excl. tax" and "Stock" are visually distinct thanks to <strong>, but they remain <td> for assistive technologies. NVDA or VoiceOver won't recognize them as headers: when navigating to the "79 €" cell, the user simply hears "79 €" without knowing which product or column it refers to.
✅ Compliant : Column and row headers correctly structured
<table>
<caption>IT equipment inventory</caption>
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col">Price excl. tax</th>
<th scope="col">Stock</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Mechanical keyboard</th>
<td>79 €</td>
<td>14</td>
</tr>
<tr>
<th scope="row">Ergonomic mouse</th>
<td>45 €</td>
<td>32</td>
</tr>
</tbody>
</table>Column headers use <th scope="col"> and row headers use <th scope="row">. When navigating to the "79 €" cell, the screen reader announces "Price excl. tax, Mechanical keyboard: 79 €". The user understands the data without returning to the top of the table or scanning the row from the beginning.
✅ Compliant : Headers declared with ARIA on a <div> table
<div role="table" aria-label="Team weekly schedule">
<div role="row">
<span role="columnheader">Member</span>
<span role="columnheader">Monday</span>
<span role="columnheader">Tuesday</span>
</div>
<div role="row">
<span role="rowheader">Alice Smith</span>
<span role="cell">Present</span>
<span role="cell">Remote work</span>
</div>
</div>When the table is built with <div> and <span> (common with front-end frameworks), the ARIA roles role="columnheader" and role="rowheader" allow headers to be declared programmatically. This approach is valid for headers covering an entire column or entire row. For a native HTML table, always prefer <th>: shorter, more robust, better supported.
Tips and pitfalls
⚠️ Bold or colored <td> doesn't count as a header
This is the number-one table auditing mistake. Dozens of tables use <td style="font-weight: bold"> or <td class="table-header"> to visually simulate a header. CSS styling creates no semantics: screen readers ignore appearance and rely solely on HTML markup. Only <th> or an explicit ARIA role produces a programmatic header.
⚠️ Row headers: the forgotten RGAA requirement
Developers think about column headers (first row) and systematically forget row headers (first column). In a table listing regions, products, or people, the first cell of each row is a header: it must be a <th scope="row">. Without this, test 5.6.2 fails, even if the entire first row is correct.
💡 role="columnheader" is not a universal substitute for <th>
The ARIA roles columnheader and rowheader only cover headers applying to an entire column or row (tests 5.6.1 and 5.6.2). For partial headers (test 5.6.3), only <th> is accepted. Adding role="columnheader" to a partial header may seem to satisfy the criterion visually, but will not be RGAA-compliant.
⚠️ Multi-level tables: criterion 5.6 is just the starting point
A table with headers merged across multiple rows (<th colspan="3"> followed by sub-headers) can pass criterion 5.6 if all these headers are <th>. But without scope, id and headers attributes, data cells won't be correctly associated with their headers. Criterion 5.7 requires these explicit associations for complex tables.
⚠️ Test 5.6.4: an ambiguous wording in the official RGAA
The methodology of test 5.6.4 contains a recognized drafting error: step 3 is a copy of the previous test's. In practice, this test verifies only that cells associated with multiple headers use <td> or <th> — not non-semantic elements. For any native HTML table, this condition is mechanically met as soon as the table is coded with standard tags.
Frequently asked questions
When should I use role="columnheader" rather than <th> to satisfy RGAA criterion 5.6?
Yes, for headers covering an entire column or row. RGAA accepts both: <th> and an element with role="columnheader". In practice, if you write native HTML, always use <th>: it's shorter, more robust, and better supported by screen readers. Reserve ARIA roles for situations where you don't control the markup (third-party component, framework generating <div>).
What is the difference between RGAA criteria 5.6 and 5.7 for tables?
Criterion 5.6 verifies that headers exist and are declared (<th> or ARIA role). Criterion 5.7 verifies that they are correctly associated with data cells via scope, id and headers attributes. A table can pass 5.6 (all headers are <th>) and fail 5.7 (associations are absent or incorrect for a complex table). For a simple single-level header table, both criteria are generally satisfied together.
How can I quickly spot table header issues during RGAA audit?
Open DevTools, click on each cell that looks visually like a header, and check in the Elements panel if it's a <th> or <td>. Includdy automatically flags <td> used as headers. In practice: if <thead> is missing or if the table's first row uses <td>, criterion 5.6 almost certainly fails.
What impact do <th colspan> and <th rowspan> have on RGAA criterion 5.6 compliance?
Yes. RGAA places no restrictions on colspan or rowspan for this criterion. A <th colspan="3">Quarter 1</th> spanning three columns is a valid header under 5.6. The association of this header to the cells it covers is handled by criterion 5.7.
Is the scope attribute mandatory to satisfy RGAA criterion 5.6?
No. Criterion 5.6 does not require the scope attribute. The presence of <th> is enough to pass tests 5.6.1 through 5.6.4. The scope attribute is required by criterion 5.7. That said, adding scope="col" and scope="row" from the start is good practice: it prepares the table to satisfy both criteria without rework.