"Jane Doe" on its own is a word in a box. Is she the instructor, a student, a guest? In a properly built table, VoiceOver says Instructor Jane Doe, column 2 of 3 - the header is what turns the word into a fact. Lose that relationship in the code and the table is just a grid of words, no matter how clear it looks on screen.
This breakdown uses the Table Headers lab (E09): Case 1 (<th> without scope) and Case 2 (scope="row" on column headers). Both use real header cells and both sound fine in VoiceOver. The evidence comes from real accessibility-tree and VoiceOver captures.
The problem
Case 1 - <th> without scope
The course table has a proper thead with three <th> cells, but none of them says which direction it labels:
<table id="table-headers-broken-case1-course-table">
<thead>
<tr>
<th id="table-headers-broken-case1-course-header">Course</th>
<th>Instructor</th>
<th>Room</th>
</tr>
</thead>
<tbody>
<tr>
<td>Intro to WAS</td>
<td>Jane Doe</td>
<td>101</td>
</tr>
<!-- ARIA Patterns / Sam Lee / 204, Keyboard Testing / Alex Kim / 115 -->
</tbody>
</table>
What the accessibility tree showed (broken)
- table "" > rowgroup "" > row "" with columnheader "Course", columnheader "Instructor" and columnheader "Room"
- Focused columnheader "Course": Role columnheader, Focusable: false - and no Scope property in the Chrome pane, because there is no
scopein the HTML - Data rows are row "" nodes with cell children (cell "Intro to WAS", cell "Jane Doe", cell "101")
Chrome inferred the column headers from position in thead.
What VoiceOver announced (broken)
Moving through the table with Control-Option and the arrow keys:
table, 3 columns, 4 rows
Course Course, column 1 of 3
row 2 of 4 Course Intro to WAS, column 1 of 3
Instructor Jane Doe, column 2 of 3
end of table
Inferred, not declared
The guess landed, but it was a guess. The W3C WAI Tables Tutorial accepts top-row <th> without scope only for small tables with clearly distinct columns; scope is needed when data is ambiguous or the table is larger. The WAS rule is simpler: put scope on every <th> whenever you can.
Case 2 - scope="row" on column headers
This is the partial fix. Someone read "add scope" and added it - with the wrong value:
<thead>
<tr>
<th scope="row" id="table-headers-broken-case2-course-header">Course</th>
<th scope="row">Instructor</th>
<th scope="row">Room</th>
</tr>
</thead>
What the accessibility tree showed (broken, Case 2)
- The header row now exposes rowheader "Course", rowheader "Instructor" and rowheader "Room" - not columnheader
- Focused rowheader "Course": Role rowheader. Chrome's pane shows no Scope property, so the
scope="row"itself is read from the markup. - The data row / cell structure is unchanged
The code now claims each of those cells labels its own row, not the column below it. The page shows one relationship; the code declares another.
What VoiceOver announced (broken, Case 2)
The lab's capture notes record VoiceOver for Case 2 as identical to Case 1, so it was not re-recorded. By ear, nothing changed. The failure only shows up in the tree.
Why this fails exam-style
- 1.3.1 Info and Relationships (Level A) requires that relationships conveyed visually can be programmatically determined.
- Case 2 declares the wrong relationship: the visual table has column headers, the code has row headers.
- Case 1 relies on browser inference. It happens to work here, but the association is inferred, not declared - which is why the WAS habit is explicit
scope. - Exam signal for a wrong scope: rowheader in the tree, confirmed by
scope="row"in the markup. Exam signal for missing versus correct scope on a simpletheadtable: the HTML, not Chrome's pane. -
<caption>andheaders/idare separate topics (Table Captions and Complex Relationships labs).
The fix
Give every column header scope="col". One attribute, on each <th> in the header row. The same fix repairs both cases:
<thead>
<tr>
<th scope="col" id="table-headers-fixed-case1-course-header">Course</th>
<th scope="col">Instructor</th>
<th scope="col">Room</th>
</tr>
</thead>
In React / JSX the attribute keeps its name. When headers run down the side instead, scope="row" belongs on the first cell of each data row (illustrative, not part of this lab):
<tr>
<th scope="row">Intro to WAS</th>
<td>Jane Doe</td>
<td>101</td>
</tr>
What changed in the tree (fixed)
- Both fixed cases expose columnheader "Course", columnheader "Instructor" and columnheader "Room"
- Case 2: the three rowheader nodes from the broken version are columnheader again; data rows unchanged
- Case 1: the tree is identical to broken Case 1. Chrome shows no Scope property for
scope="col"(live capture 2026-06-15). For missing versus correct scope, read the markup.
What VoiceOver announced (fixed)
Case 1:
table, 3 columns, 4 rows
Course, column 1 of 3
Contrast broken Case 1, where the same header read Course Course, column 1 of 3.
Course Intro to WAS, column 1 of 3
You are currently on a selectable text, inside of a cell.
To be honest about the evidence: Instructor Instructor, column 2 of 3 and Room Room, column 3 of 3 still double on the fixed table, so the doubling is not a reliable pass or fail signal.
Case 2:
table, 3 columns, 4 rows
You are currently on a selectable text, inside of a column header.
Behaviors to verify
- Every header cell is a
<th>, not a styled<td>, and its text describes the data it labels. - Every column header has
scope="col"; every row header hasscope="row"; noscope="row"sits on column labels inthead. - In the accessibility tree, column labels are columnheader and row labels are rowheader. Then move through a data row with a screen reader and confirm each cell is read with the right header. Do not trust your ears alone.
Practice it
- Video: https://youtu.be/cTSw8wnJwcM
- Short: https://youtube.com/shorts/y65zZN_R23k
- Lab: Table Headers lab
- Companion write-up: https://www.wasexamprep.com/blog/table-without-headers-is-a-grid-of-words
Takeaway: a table without declared headers is just a grid of words. Put scope on every th, and check the tree, not just your ears.
If this was useful, subscribe to A11Y in Practice on YouTube (@a11yinpractice) for more hands-on accessibility episodes, and visit wasexamprep.com for WAS study notes and labs.
A11Y in Practice and wasexamprep.com are independent educational resources. They are not affiliated with or endorsed by IAAP (International Association of Accessibility Professionals).
Top comments (0)