November 4, 2026
Accessible data tables in HTML: th, scope, headers and caption with code examples
Accessible data tables in HTML rest on one simple principle: every header cell is marked up with <th>, every data cell with <td>, and the markup defines the relationship between the two; for more complex tables, explicit associations may be needed. That is the core message of the W3C (WAI) Tables Tutorial: assistive technologies rely on this markup to give users context. Depending on how complex the table is, you add the scope attribute, or the id and headers attributes, and, in most situations, a <caption> element that gives the table a title. This guide walks through each case with code you can adapt: a table with one header, a table with row and column headers, spanning headers, a complex multi-level table, captions and summaries, and the mistakes to avoid.
Why markup matters for accessible data tables in HTML
A table without structural markup, one that does not differentiate header cells from data cells and link them properly, creates accessibility barriers. Relying on visual cues alone is not enough: headers and data cells need to be programmatically determinable by software.
In practice, a screen reader speaks one cell at a time and announces the row and column headers associated with it, so the user does not lose context while moving through the table. Other people render the data differently, for example with custom stylesheets that make header cells more prominent, change text size and colors, or display the information as lists rather than grids. These alternative renderings only work if the table code is properly structured.
On the WCAG side, the W3C tutorial maps these practices to success criterion 1.3.1 Info and Relationships (Level A) and to techniques H51 (using table markup to present tabular information), H63 (using the scope attribute to associate header cells with data cells in data tables) and H39 (using caption elements to associate data table captions with data tables).
- Header cells: <th>.
- Data cells: <td>.
- More complex tables: explicit associations using scope, id and headers may be needed.
- These rules apply to data tables, not to layout tables.
Simple tables: <th> for headers, scope to remove ambiguity
The simplest case is a table with a single header, either in the top row or in the first column, where the data in each column is clearly distinct. For a small table of concerts (date, event, venue), marking up the first row with <th> and the rest with <td> is only acceptable because the table is so small and the data in each column is distinctly different, which makes the relationship between headers and data evident. The W3C notes, however, that some screen readers will read “Date – Event – Venue” in the “Venue” cell, because the direction of the <th> elements is ambiguous.
As soon as the data no longer speaks for itself, add the scope attribute. In the W3C example, a list of collectors with last name, first name and city, you cannot tell a last name from a first name or a city without knowing which header each cell belongs to. The col value of scope defines the direction of the header cells and associates them with the data cells in their column. The W3C adds that scope is also needed for larger tables with only one header row or column.
<table>
<caption>Teddy bear collectors</caption>
<tr>
<th scope="col">Last Name</th>
<th scope="col">First Name</th>
<th scope="col">City</th>
</tr>
<tr>
<td>Phoenix</td>
<td>Imari</td>
<td>Henry</td>
</tr>
[…]
</table>Row and column headers: scope="col" and scope="row"
When a table has both a column header and a row header, the relationship between headers and data quickly becomes ambiguous. Use <th> for every header cell and declare the direction of each one: scope="col" means the header applies to the whole column, scope="row" to the whole row.
The W3C example is a table of delivery slots: weekdays across the top, time ranges down the left, and “Open” or “Closed” in the cells. Those labels are only useful if they can be tied to a specific day and time. The empty top-left cell stays a <td>.
The same technique works when row headers are not in the first column. In a table of holidays where the first column holds an ID and the second the person's name, the name is marked up as <th scope="row">, so it applies to the cells on its left and on its right.
<table>
<caption>Delivery slots</caption>
<tr>
<td></td>
<th scope="col">Monday</th>
<th scope="col">Tuesday</th>
</tr>
<tr>
<th scope="row">09:00 – 11:00</th>
<td>Closed</td>
<td>Open</td>
</tr>
[…]
</table>Spanning headers: colgroup, rowgroup, thead and tbody
When a header cell spans several columns (colspan) or rows (rowspan), it should be associated with the corresponding cells in the group, using scope="colgroup" or scope="rowgroup". Before that, the structure of those groups has to be defined in the markup.
For columns, each column is identified by a <col> element, starting from the left; if a header spans several columns, one <colgroup> with a span attribute replaces that number of <col> elements. The <col> elements not inside a <colgroup>, plus the columns indicated by the span attributes of the <colgroup> elements, should add up to the total number of columns in the table. For rows, a group is defined by <thead>, <tbody> or <tfoot>.
- <thead> and <tfoot> can be used once per table.
- A table can have any number of <tbody> elements, each defining a row group.
- If a header spans several header rows, wrap those rows in <thead> instead of <tbody>; if it spans several rows in the footer area, use <tfoot>.
- The W3C notes that using <thead>, <tbody> and <tfoot> in every table may avoid confusion about when to use them.
<table>
<col>
<colgroup span="2"></colgroup>
<colgroup span="2"></colgroup>
<tr>
<td rowspan="2"></td>
<th colspan="2" scope="colgroup">Mars</th>
<th colspan="2" scope="colgroup">Venus</th>
</tr>
<tr>
<th scope="col">Produced</th>
<th scope="col">Sold</th>
<th scope="col">Produced</th>
<th scope="col">Sold</th>
</tr>
<tr>
<th scope="row">Teddy Bears</th>
<td>50,000</td>
<td>30,000</td>
<td>100,000</td>
<td>80,000</td>
</tr>
[…]
</table>Complex tables: linking each cell with headers and id
Some tables are too complex for header and data cells to be associated in a strictly horizontal or vertical way. The W3C names two cases: tables whose column headers repeat or change part-way through, and tables with three or more header cells associated with each data cell.
The method: each <th> gets an id that is unique across the whole document, and each <td> lists, in its headers attribute, the id or ids of the relevant header cells, separated by spaces. In the W3C holiday accommodation example, the cell “11” is linked to the city (Paris), the number of bedrooms (1 bedroom) and the accommodation type (studio) at the same time.
A tip from the tutorial: in a supplier contacts table whose headers change halfway down, the header cells themselves point to an empty cell with the id “blank”, which prevents some assistive technologies from reading a header for those header cells.
Before going down this road, ask whether the table really needs to be that complex. The W3C suggests that in many cases it is worth splitting it into smaller tables: the information becomes easier for everyone to understand and easier to code, and simple tables are much better supported by content authoring tools, including WYSIWYG editors.
[…]
<tr>
<th id="par" colspan="5" scope="colgroup">Paris</th>
</tr>
<tr>
<th id="pbed1">1 bedroom</th>
<td headers="par pbed1 stud">11</td>
<td headers="par pbed1 apt">20</td>
[…]
</tr>Captions with <caption> and summaries for complex tables
A caption works like a heading for the table. Most screen readers announce it; it helps users find the table, understand what it is about and decide whether to read it. For people navigating in “Tables Mode”, jumping from table to table, captions are the primary way to identify tables. Use the <caption> element, placed directly as a child of <table>, and keep it a short heading, such as “Concerts”.
A summary describes how the data is organized and helps users navigate; it is usually only needed for complex tables. If you provide both, the summary should not duplicate information already in the caption. The W3C points out that captions and summaries are not required in every case to meet WCAG, but they are fairly straightforward ways to provide information that is often needed.
- Summary inside <caption>: the text follows the title, for example in a <span>, and is visible to sighted users too.
- Summary linked with aria-describedby on <table>, pointing to any element with a unique id, such as a paragraph. The element does not need to come before the table, but placing it near the table helps users discover the summary more quickly, especially if they are not using a screen reader. The W3C warns that this WAI-ARIA feature may be less widely supported by assistive technology.
- Table wrapped in <figure> with <figcaption>: in “Tables Mode”, screen reader users are usually unable to identify the table by a caption applied this way. The caption part of <figcaption> can be explicitly associated with the table using aria-labelledby, and the summary part using aria-describedby, which could lead to the caption and summary being read out multiple times. The W3C also warns that this HTML5 feature may be less widely supported by assistive technology.
<table>
<caption>Availability of holiday accommodation<br>
<span>Column one has the location and size of accommodation, other columns show the type and number of properties available</span>
</caption>
[…]
</table>What not to do in an HTML data table
The tutorial's “Tips and Tricks” page brings together several practices to avoid.
- Styling <td> cells to look like headers: use <th>, otherwise the table becomes less accessible, if not inaccessible. It also helps to differentiate <th> and <td> visually.
- Merging several tables into one with an extra row of <th> cells: screen readers may read aloud all <th> cells in a column. Start a new <table> when the topic changes.
- Putting headers in one column and all the data in a second: every piece of data needs its own cell.
- Creating pseudo-rows with <br>: the data in those pseudo-rows may no longer align correctly when text is resized.
- Cutting the table off on small screens or when zoomed, for example with overflow: hidden in CSS. The tutorial applies overflow: scroll to an element wrapping the table, but there are many other display options.
- Losing the structure in responsive layouts: the relationship between headers and data must be available in every format.
- Using tables for layout: use CSS instead. If layout tables already exist, use neither the structural elements (like <th> or <caption>) nor the attributes covered in the tutorial (scope, headers…), and add role="presentation" to the <table> element.
Frequently asked questions
How do I make an HTML table accessible?
Mark up header cells with <th> and data cells with <td>. Add scope="col" or scope="row" when the direction of headers is ambiguous, scope="colgroup" or scope="rowgroup" for spanning headers, and id and headers when the association cannot be strictly horizontal or vertical (headers that change part-way through, three or more headers per cell). In most situations, also give the table a title with <caption>: WCAG does not require it in every case, but it is useful.
What is the difference between scope and headers?
scope states the direction of a header: col, row, colgroup or rowgroup. It is enough as long as the association is strictly horizontal or vertical. headers, usually placed on a data cell, explicitly lists the ids of the associated header cells; use it when headers change part-way through the table or when three or more headers apply to each cell.
Is the caption element required in a table?
According to the W3C, captions and summaries are not required in every case to meet WCAG. A caption is still useful in most situations: most screen readers announce it, and it is the primary way to identify a table in Tables Mode.
Do I need scope on a simple table?
Not always: <th> without scope is only acceptable for a small table whose data is clearly distinct in each column, and even then some screen readers will read “Date – Event – Venue” in the “Venue” cell because the direction of the <th> elements is ambiguous. As soon as the data is ambiguous, or for a larger table with a single header row or column, add scope.
Can I use an HTML table for layout?
No, the W3C recommends CSS for layout. For an existing layout table, use neither structural elements such as <th> or <caption> nor the attributes covered in this guide (scope, headers…), and add role="presentation" to the <table> element.
Further reading on Conformly
Sources
This article relies on the official sources listed at the end of the page.