Missing Table Headers: Why Data Tables Need th, Not Just Bold Text
Look at any data table with your eyes, and the header row is obvious — it's usually bold, maybe shaded differently, sitting at the top or left edge. A screen reader user navigating cell by cell doesn't get that visual context for free; without real markup connecting each data cell to its header, moving through a 40-row table means hearing a wall of bare numbers and words with no idea what any of them mean.
This falls under WCAG Success Criterion 1.3.1 (Info and Relationships), Level A.
The fix for simple tables
Use <th> for header cells instead of a styled <td>, and set scope to say whether the header applies to its column or its row:
<!-- Before: headers are just bold text, not real header cells -->
<table>
<tr><td><b>Name</b></td><td><b>Revenue</b></td></tr>
<tr><td>Acme Corp</td><td>$1.2M</td></tr>
</table>
<!-- After: real header cells, explicitly scoped -->
<table>
<thead>
<tr><th scope="col">Name</th><th scope="col">Revenue</th></tr>
</thead>
<tbody>
<tr><td>Acme Corp</td><td>$1.2M</td></tr>
</tbody>
</table>
A screen reader can now announce "Revenue: $1.2M" when landing on that cell, instead of just "$1.2M" — the scope="col" association is what supplies the column name automatically.
Complex tables need explicit id/headers pairing
Simple scope breaks down for tables with multi-level or irregular headers (a table with both row and column headers, or merged header cells spanning several columns). For those, each <th> gets a unique id, and each data cell lists exactly which header IDs apply via the headers attribute:
<table>
<tr>
<th id="q1">Q1</th>
<th id="q2">Q2</th>
</tr>
<tr>
<th id="rev">Revenue</th>
<td headers="rev q1">$1.2M</td>
<td headers="rev q2">$1.4M</td>
</tr>
</table>
This is more verbose, but it's the only reliable way to disambiguate a data cell's meaning in a genuinely multi-dimensional table.
The one thing to check first: is this even a data table?
Tables used purely for visual layout (a common pattern in older sites, and in some auto-generated or email-exported HTML) shouldn't have <th> elements added to them at all — that would incorrectly announce layout cells as data headers. If a table has no logical rows-and-columns relationship to communicate, the better fix is usually to replace the table entirely with semantic non-table markup (divs and CSS Grid/Flexbox), not to retrofit header markup onto something that was never really tabular data.
Official references
- Deque University: table-fake-caption
- Deque University: td-has-header
- W3C Technique H43
- W3C Technique H51
- W3C Technique H63
- W3C Technique H73
Common questions
- Why isn't bold text enough for table headers?
- Bold or shaded text is only a visual cue. A screen reader navigating cell by cell needs real `<th>` markup to announce which header a data cell belongs to; styling alone conveys nothing to it.
- How do I mark up table headers correctly?
- Use `<th>` for header cells and add `scope=col` or `scope=row` so each data cell is associated with the right header. For complex tables, connect cells with `headers` and `id`.
- Does this apply to layout tables?
- No — this is about data tables. Tables used purely for visual layout should be replaced with CSS and never need `<th>`; the requirement is for tables that present real tabular data.
Related articles
Want to see how your own site scores?
Run a free accessibility scan