<th> with scope attributes, a <caption>, and <thead>/<tbody>. Layout tables must have role="presentation" and no table header elements.
Screen readers navigate tables differently than sighted users. They read cell by cell and need explicit header associations to announce "row: Product name, column: Price — $29" rather than just "$29" with no context. Without proper markup, data tables are meaningless to blind users.
Data tables present structured information — pricing, schedules, comparisons, statistics. They need full accessibility markup.
See how 321 websites scored →
View the 2026 ReportLayout tables (using <table> for visual positioning) are a legacy pattern that should be avoided. If you must use one, remove all semantic meaning:
<table role="presentation">
<tr>
<td>Left column content</td>
<td>Right column content</td>
</tr>
</table>
<!-- BAD: no headers, no caption -->
<table>
<tr><td>Plan</td><td>Price</td><td>Users</td></tr>
<tr><td>Starter</td><td>$9</td><td>1</td></tr>
<tr><td>Pro</td><td>$29</td><td>5</td></tr>
</table>
<!-- GOOD: headers, scope, caption, thead/tbody -->
<table>
<caption>Pricing plans comparison</caption>
<thead>
<tr>
<th scope="col">Plan</th>
<th scope="col">Monthly price</th>
<th scope="col">Users included</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Starter</th>
<td>$9</td>
<td>1</td>
</tr>
<tr>
<th scope="row">Pro</th>
<td>$29</td>
<td>5</td>
</tr>
</tbody>
</table>
The scope attribute tells screen readers which cells a header applies to:
scope="col" — this header applies to cells in its columnscope="row" — this header applies to cells in its rowscope="colgroup" — applies to a group of columns (for complex tables)scope="rowgroup" — applies to a group of rowsThe <caption> element is the accessible name for the table. Screen readers announce it when the user navigates to the table. It must be the first child of <table>.
<table> <caption>Q1 2026 sales figures by region</caption> ... </table>
If you don't want the caption to be visible, you can visually hide it while keeping it accessible:
caption.sr-only {
position: absolute; width: 1px; height: 1px;
overflow: hidden; clip: rect(0,0,0,0);
}
For tables with row and column spans (irregular headers), use id and headers attributes instead of scope:
<table>
<caption>Sales by product and quarter</caption>
<thead>
<tr>
<th id="product">Product</th>
<th id="q1" colspan="2">Q1</th>
<th id="q2" colspan="2">Q2</th>
</tr>
<tr>
<td></td>
<th id="q1-units" headers="q1">Units</th>
<th id="q1-rev" headers="q1">Revenue</th>
<th id="q2-units" headers="q2">Units</th>
<th id="q2-rev" headers="q2">Revenue</th>
</tr>
</thead>
<tbody>
<tr>
<th id="widget-a" headers="product">Widget A</th>
<td headers="q1 q1-units widget-a">320</td>
<td headers="q1 q1-rev widget-a">$9,600</td>
<td headers="q2 q2-units widget-a">410</td>
<td headers="q2 q2-rev widget-a">$12,300</td>
</tr>
</tbody>
</table>
Responsive tables that hide columns on mobile must maintain header associations even when columns are hidden or reordered via CSS. Never use CSS to visually reorder table cells in a way that breaks the DOM order, as screen readers read DOM order.
<td> for all cells including headersscope attribute on <th> elements<caption> on data tablesrole="presentation"colspan/rowspan) without headers attributesAccessalyze detects missing table headers, scope attributes, and other WCAG 1.3.1 violations automatically.
Scan for Table Issues →See real website accessibility scores: Browse 244+ free accessibility audits →
Try it yourself
Enter your website URL to get a free accessibility score.