Accessible Data Tables and Complex Content
Tables are among the most important tools for organizing data clearly - and at the same time among the most frequently mis-marked-up structures on the web. Prices by plan, timetables, comparison matrices or financial figures: visually the eye recognizes the relationship between header and cell instantly. A screen reader, by contrast, reads each cell individually and depends on the relationship between header and data cell being encoded technically. How rarely this succeeds is shown by the WebAIM Million Report: only 16.8% (WebAIM Million, 2024) of the over one million detected tables had valid data table markup. This guide shows how to build tables with th, scope, caption and, for complex tables, headers and id so that the relationships are preserved programmatically - as a solid basis for your accessible web development.
Why Data Tables So Often Fail on Accessibility
For sighted users a table is a two-dimensional grid: the eye jumps between column header, row header and data cell and establishes the relationship almost incidentally. A screen reader has no such spatial overview. It reads cell by cell and must know, for each data cell, which column and row header it belongs to. This is exactly the relationship that WCAG 1.3.1 Info and Relationships demands: information, structure and relationships conveyed visually must be programmatically determinable (W3C/WAI). Without the markup, the user hears a sequence of disconnected values.
Practice shows how large the gap is. In the WebAIM Million Report, 95.9% (WebAIM Million, 2024) of all home pages analyzed in 2024 had detectable WCAG failures, averaging 56.8 (WebAIM Million, 2024) per page - an increase of 13.6% (WebAIM Million, 2024) over the previous year. Tables are a problem case of their own: if only 16.8% (WebAIM Million, 2024) of tables are validly marked up at all, the majority remain incomprehensible to assistive technologies. In Germany this affects many people directly: at the end of 2023 around 7.9M (Federal Statistical Office, 2024) people with severe disabilities lived here, and worldwide around 2.2B (WHO, 2023) people have a vision impairment.
A table is not automatically a data table
The Building Blocks of an Accessible Data Table
An accessible data table emerges from the interplay of a few clearly defined building blocks. The following overview summarizes what the Tables Tutorial of the Web Accessibility Initiative and WCAG 1.3.1 require (W3C/WAI). We check these points systematically as part of our services.
th for header cells
Header cells are marked up with th, data cells with td. This lets assistive technology distinguish heading from content.
scope attribute
scope=col and scope=row define whether a header cell applies to a column or a row. It is the simplest form of association.
caption
The caption gives the table a visible title and serves screen readers as a landmark. It comes directly after the table tag.
headers and id
For complex tables, unique id values on th and the headers attribute on td link each cell to its headers.
thead, tbody, tfoot
These structural elements group the header, body and footer area and improve navigation and understanding of the table.
No layout tables
Misusing tables purely for layout is a WCAG failure as soon as they contain th, caption or summary (W3C/WAI, F46).
For most tables the combination of th, scope and caption is fully sufficient. Only when header cells span several columns or rows, or when multiple heading levels interlock, do headers and id come into play. This semantic markup builds on the same foundations we cover in detail in our article on screen reader optimization for websites.
Simple Tables: th, scope and caption
The most common type of table has exactly one row of column headers, often complemented by a column of row headers. Here it is enough to mark up the header cells with th and indicate the direction with scope: scope=col for column headers, scope=row for row headers. The screen reader then reads the matching headers for each data cell - for example column Monthly, row Pro, value 19 Euro. The caption gives the whole table a title that assistive technologies offer as an entry point.
<table>
<caption>Prices by plan and term</caption>
<thead>
<tr>
<th scope="col">Plan</th>
<th scope="col">Monthly</th>
<th scope="col">Yearly</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Basic</th>
<td>9 Euro</td>
<td>90 Euro</td>
</tr>
<tr>
<th scope="row">Pro</th>
<td>19 Euro</td>
<td>190 Euro</td>
</tr>
</tbody>
</table>scope is the underrated key
Complex Tables: Linking headers and id Correctly
As soon as header cells span several columns or rows (colspan, rowspan) or several heading levels exist, scope is no longer enough. Then the association is no longer strictly horizontal or vertical, and the relationship has to be established explicitly (W3C/WAI). For this, each header cell gets a unique id, and each data cell lists in its headers attribute the id values of all headers that apply to it - separated by spaces when there are several. The screen reader then reads exactly the associated headers for each cell and does not lose context.
<table>
<caption>Revenue by region and quarter</caption>
<tr>
<td></td>
<th id="q1" scope="col">Q1</th>
<th id="q2" scope="col">Q2</th>
</tr>
<tr>
<th id="north" scope="row">North</th>
<td headers="north q1">120</td>
<td headers="north q2">140</td>
</tr>
<tr>
<th id="south" scope="row">South</th>
<td headers="south q1">95</td>
<td headers="south q2">110</td>
</tr>
</table>It is important to avoid complex tables where possible: nested headers can often be split into several simple tables, each of which works with scope alone. A simple, clearly structured table is not only easier to mark up but also more understandable for all users. Correctly linking multiple relationships via id and headers is closely related to the clean assignment of roles and states described in our article on ARIA roles, states and live regions.
| Table type | Suitable markup | Typical mistake |
|---|---|---|
| Simple table (one header row) | th with scope=col, optionally scope=row, caption | td instead of th for headers, no scope |
| Table with row and column header | th scope=col plus th scope=row, caption | scope missing, relationship only visual |
| Complex table (colspan/rowspan) | unique id on th, headers on td | nested th without headers/id |
| Layout table (visual only) | better CSS grid/flexbox, no th/caption | th, caption or summary in layout (F46) |
| Responsive table (mobile) | preserve relationships, do not dissolve | hiding columns, relationships are lost |
Layout Tables vs. Data Tables
Historically, tables were often misused to lay out pages - with rows and columns as a design grid instead of a data structure. Today this is a recognized failure: a table that serves only the layout but contains th elements, a caption or a non-empty summary attribute violates WCAG 1.3.1 because it uses semantic markup purely for presentation (W3C/WAI, F46). Screen readers announce such a table as a data table and confuse the user with row and column announcements where there is no data at all.
The rule is simple: use table markup only for genuinely tabular data. For pure layout, CSS grid and flexbox are the right choice because they separate presentation from structure and retain the semantic meaning of the HTML table elements (W3C/WAI). If a layout table is exceptionally necessary, it must contain neither th nor caption nor summary and should be neutralized with role=presentation. The clean separation of content and presentation is also a central topic in our article on accessible forms and validation.
Real data deserves real tables
Responsive Tables and Very Wide Data Sets
Wide data tables are a challenge on small screens. The most common but problematic solution hides columns on mobile or breaks the table into cards - and often loses the relationship between header and value in the process. This is exactly what the Tables Tutorial of the Web Accessibility Initiative warns against: stacking or hiding columns must not destroy the programmatic relationships (W3C/WAI). If a responsive layout turns a table into a stacked layout, the headers must remain available for each value.
Two approaches have proven effective: first, keeping the table in a horizontally scrollable container so its structure stays untouched - the container should be keyboard focusable and given an accessible name. Second, with a card layout, visually repeating the header per value, for instance via data attributes and CSS, so it remains clear which value belongs to what. From supporting 50+ (project experience) projects we know that most table problems arise not on the desktop but in the mobile implementation - where columns are simply hidden under time pressure.
Common mistake: simply hiding columns
- Header cells marked up with th instead of td
- scope=col and scope=row set for the direction of the headers
- caption present as a visible table title
- Complex tables linked via unique id and headers
- thead, tbody and, where applicable, tfoot used for grouping
- Layout tables avoided or neutralized with role=presentation
- Responsive without losing the header-value relationship
- Verified with keyboard and screen reader
A table is only accessible once every value knows its header. Whoever builds only the grid but forgets the relationship delivers numbers without meaning.
Data tables touch above all WCAG 1.3.1 Info and Relationships, because here the entire column and row logic must be programmatically available (W3C/WAI). Since automated testing tools can only test about one third to one half of WCAG requirements at all (W3C/WAI), it only shows in a manual test with a screen reader whether a table really conveys its relationships. Whoever builds tables from the start with th, scope, caption and, where needed, headers and id avoids an entire class of recurring errors - and creates a reliable basis for the WCAG audit. We cover related patterns for dynamic and overlaying content in our articles on accessible modals and overlays and accessible error messages and status messages.