HTML tables used to present data need structured markup that goes beyond visual formatting. When a sighted user scans a table, they can glance at column and row headers to understand what each cell means. Screen reader users don't have that luxury. They rely on programmatic associations between header cells (<th>) and data cells (<td>) to navigate and interpret table content. Without these associations, a screen reader reads cells in sequence with no context, turning structured data into a stream of disconnected values.
Accessible table markup involves several HTML elements and attributes working together: <caption> provides a visible or programmatically available title for the table, <th> identifies header cells, the scope attribute clarifies whether a header applies to a column or row, and the headers attribute creates explicit associations in complex tables where scope alone is insufficient. Getting these right is both a WCAG requirement (primarily Success Criterion 1.3.1, Info and Relationships) and an HTML validation concern, since misusing table elements triggers warnings and errors in validators.
Why data tables and accessibility markup matters
Screen reader users navigate tables cell by cell using keyboard shortcuts. As they move through a table, the screen reader announces the associated header for each data cell. If a table lacks <th> elements or scope attributes, the screen reader cannot make these announcements, and the user hears raw values like "42" or "Yes" with no indication of what column or row those values belong to.
This affects blind and low vision users directly, but it also affects users of other assistive technologies and anyone who relies on the accessibility tree for information. Search engines and content extraction tools also benefit from properly structured tables.
Common problems include using <td> for everything (including headers), relying on visual styling like bold text to indicate headers instead of semantic markup, omitting <caption>, and using layout tables where CSS layout would be more appropriate.
How data tables and accessibility markup works
Simple tables with scope
For tables with a single row of column headers or a single column of row headers, the scope attribute on <th> is enough to create clear associations. Setting scope="col" tells assistive technology that the header applies to all cells in that column. Setting scope="row" does the same for rows.
The <caption> element should appear as the first child of <table>. It gives the table a name, which screen readers announce when the user encounters the table, helping them decide whether to explore it.
Complex tables with headers
When a table has merged cells (using colspan or rowspan), multiple levels of headers, or an irregular structure, scope can become ambiguous. In these cases, assign an id to each <th> and use the headers attribute on each <td> to list the id values of all headers that apply to that cell, separated by spaces.
This approach is more verbose but gives screen readers an unambiguous mapping from every data cell to its headers.
Layout tables vs. data tables
Tables used purely for visual layout should not have <th>, <caption>, or summary attributes. Adding role="presentation" or role="none" to a layout table tells assistive technology to ignore the table semantics. However, CSS-based layouts are strongly preferred over layout tables.
Code examples
Bad example: table with no header markup
<table>
<tr>
<td>Name</td>
<td>Department</td>
<td>Extension</td>
</tr>
<tr>
<td>Ava Chen</td>
<td>Engineering</td>
<td>4012</td>
</tr>
<tr>
<td>Marcus Hall</td>
<td>Design</td>
<td>4037</td>
</tr>
</table>
A screen reader reading this table has no way to know that "Name", "Department", and "Extension" are headers. When the user reaches the cell "4037", the reader announces "4037" with no context.
Good example: simple table with <caption> and scope
<table>
<caption>Staff directory</caption>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Department</th>
<th scope="col">Extension</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ava Chen</td>
<td>Engineering</td>
<td>4012</td>
</tr>
<tr>
<td>Marcus Hall</td>
<td>Design</td>
<td>4037</td>
</tr>
</tbody>
</table>
Now the screen reader can announce "Extension: 4037" when the user navigates to that cell. The <caption> lets the user know the table's purpose before they start exploring it.
Good example: complex table with headers
<table>
<caption>Quarterly sales by region</caption>
<thead>
<tr>
<td></td>
<th id="q1" scope="col">Q1</th>
<th id="q2" scope="col">Q2</th>
</tr>
</thead>
<tbody>
<tr>
<th id="north" scope="row">North</th>
<td headers="north q1">$120k</td>
<td headers="north q2">$135k</td>
</tr>
<tr>
<th id="south" scope="row">South</th>
<td headers="south q1">$98k</td>
<td headers="south q2">$110k</td>
</tr>
</tbody>
</table>
Each <td> explicitly lists the id values of its column header and row header. A screen reader navigating to "$135k" can announce "North, Q2: $135k." In a simple two-axis table like this one, scope alone would suffice, but the headers attribute is shown here to illustrate the technique for cases where merged cells or multi-level headers make scope ambiguous.
Bad example: layout table with misleading semantics
<table>
<caption>Page layout</caption>
<tr>
<th>Sidebar</th>
<th>Main content</th>
</tr>
<tr>
<td>Navigation links here</td>
<td>Article body here</td>
</tr>
</table>
This table is used for layout, not data. The <caption> and <th> elements cause screen readers to treat it as a data table and announce header associations that are meaningless. If a table must be used for layout, strip all data table semantics:
<table role="presentation">
<tr>
<td>Navigation links here</td>
<td>Article body here</td>
</tr>
</table>
Better still, replace the layout table with CSS Grid or Flexbox.
Related terms
Help us improve this glossary term
Scan your site
Rocket Validator scans thousands of pages in seconds, detecting accessibility and HTML issues across your entire site.