Table headers must link to data cells
Last updated:
Related Guides
Table header cells must programmatically associate with the data cells they describe
Intro:
Header cells in data tables must be coded so assistive technologies can determine which headers apply to each data cell. This issue appears in data tables on dashboards, reports, and admin grids, and it primarily impacts screen reader users and people relying on voice control.
Why It Matters
When the relationship between headers and data is unclear, screen readers read values without context. Users cannot tell what a number or label refers to.
Large or dense tables become unusable when headers are not exposed programmatically. This increases cognitive load and causes errors.
Clear associations also help users of voice commands target cells by their headers and aid people navigating by table shortcuts.
Common Causes
- Using
<th>without scope, or using the wrong scope value - Placing the headers attribute on
<th>instead of on related<td> - Complex tables with multi-level headers but no id/headers mapping
- Styling layout tables to look like data tables and adding
<th>unnecessarily - Adding ARIA roles (grid/rowheader/columnheader) to non-table markup instead of using native table elements
- Missing table structure elements like
<caption>,<thead>,<tbody>, and logical reading order
How to Fix
- Confirm it’s a data table:
- If the table conveys data with row/column relationships, use semantic table markup. If it’s only for layout, remove table semantics and do not use
<th>.
- If the table conveys data with row/column relationships, use semantic table markup. If it’s only for layout, remove table semantics and do not use
- Mark real headers with
<th>:- Use
<th>only for cells that label a column or a row.
- Use
- Simple tables (single header row and/or single header column):
- Add
scope="col"to column header cells in the header row. - Add
scope="row"to row header cells in the first column. - Optionally group with
<thead>,<tbody>, and add a descriptive<caption>.
- Add
- Grouped headers (header spans a group of columns or rows):
- Use
scope="colgroup"orscope="rowgroup"on header cells that label a group. - Ensure the visual grouping matches the logical grouping.
- Use
- Complex/irregular headers (multi-level or non-rectangular associations):
- Give each
<th>a unique id. - On each
<td>, add a headers attribute listing the relevant header ids (space-separated). - Do not put headers on
<th>; headers belongs on data cells.
- Give each
- Keep header text accessible:
- Header cells must contain or reference readable text (not only icons). Use
aria-labelor visually hidden text if needed.
- Header cells must contain or reference readable text (not only icons). Use
- Avoid unnecessary ARIA:
- Prefer native HTML tables over ARIA grid roles. Only use role="columnheader"/"rowheader" when creating accessible patterns without
<table>(not recommended for typical data).
- Prefer native HTML tables over ARIA grid roles. Only use role="columnheader"/"rowheader" when creating accessible patterns without
Recommendation: Always provide a concise <caption> that describes the table’s purpose.
How to Test
Keyboard check:
- Ensure the table is reachable. If interactive elements exist inside cells, they are focusable in a logical order.
Screen reader check (NVDA, JAWS, or VoiceOver):
- Move into the table and use table navigation (e.g., Ctrl+Alt+Arrow keys).
- On any data cell, confirm the screen reader announces the correct row and column header(s).
- For complex tables, verify multiple headers are read in a meaningful order.
Mobile/touch check (VoiceOver/TalkBack):
- Explore the table by swipe and rotor controls. Confirm headers are announced with each data value.
Quick checklist:
- Each
<th>maps to the correct<td>cells via scope or id/headers. - No headers attribute appears on
<th>; headers is only on<td>. - Multi-level headers use id on
<th>and headers on<td>(when scope alone is insufficient). - Header text is present and perceivable (not icon-only).
- Table structure uses
<caption>,<thead>,<tbody>appropriately. - No ARIA grid roles unless building an interactive grid pattern.
Good Example
<table>
<caption>Quarterly sales by region (in USD)</caption>
<thead>
<tr>
<th></th>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
<th scope="col">Q3</th>
<th scope="col">Q4</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">North</th>
<td>125,000</td>
<td>138,500</td>
<td>144,200</td>
<td>152,900</td>
</tr>
<tr>
<th scope="row">South</th>
<td>98,400</td>
<td>102,700</td>
<td>110,050</td>
<td>117,300</td>
</tr>
</tbody>
</table>Bad Example
<table>
<caption>Attendee list</caption>
<tr>
<th scope="row">Name</th>
<th scope="row" headers="dept">Department</th>
<th>Email</th>
</tr>
<tr>
<td>A. Patel</td>
<td>Engineering</td>
<td>apatel@example.com</td>
</tr>
</table>What’s wrong:
- Top-row headers incorrectly use
scope="row"instead ofscope="col". - headers attribute is incorrectly placed on
<th>(it belongs on<td>when used). - One header cell (<th>Email</th>) lacks any scope, so associations are unclear.
Quick Checklist
- Use
<th>only for header cells; use<td>for data. - For simple tables, set
scope="col"and/orscope="row"on<th>. - For grouped headers, consider scope="colgroup"/"rowgroup".
- For complex tables, map with unique id on
<th>and headers on related<td>. - Do not place headers on
<th>. - Ensure header text is present and meaningful.
- Provide a clear
<caption>and logical <thead>/<tbody> structure.