Referred HTML Tags

<table>, <tr>, <th>, <caption>

Essential A11y Requirements

Tables are unique HTML elements. They are the most complex native HTML element in terms of structure and the variety of element types required to implement even the most basic setup. In fact, the correct structure of a table is so crucial for it to be correctly displayed by the browser, that modern browsers are "fixing" tables' HTML structure if needed before adding them to the DOM tree.

An example to code "fixes" browsers are making to table elements, in this example table elements are wrapped with t-body tag

In this entry, we will focus less on the structure of the tables and more on the elements and attributes that allow screen reader users to associate cells with the context of a specific column or row. Note that this entry does not review all existing HTML table elements but only those that directly affect accessibility.


An accessible table is a table that allows the user to understand if it is divided into columns, rows, or both, and also understand which column/row each cell in the table is associated with. The average user makes visual associations between the table cell's data and its column and/or row headers. However, conveying the same information by a screen reader to a person who cannot see the table's layout, requires the HTML structure to be more explicit about this association.

The way to associate a column or row header to its content is through the <th> tag. The "th" stands for "table headers". They make the context of each column or row they are in and thus allow screen readers to read them as the user switches between columns and rows. This allows the user to better understand the context of the cell contents of the current table-cell. The <th> elements are essential in making tables accessible to screen reader users, especially when it comes to large tables with a significant amount of data. Screen reader users can lose context very quickly. Most modern screen readers are "smart enough" to analyze whether the <th> is supposed to be the header of a row or column by the HTML structure of the table, like the tables below, for example.

1<!-- Columns based table -->
3 <tr>
4 <th>First Name</th>
5 <th>Last Name</th>
6 <th>Age</th>
7 </tr>
8 <tr>
9 <td>John</td>
10 <td>Doe</td>
11 <td>35</td>
12 </tr>
13 <tr>
14 <td>Jane</td>
15 <td>Melan</td>
16 <td>38</td>
17 </tr>
18 <tr>
19 <td>Ben</td>
20 <td>Foster</td>
21 <td>29</td>
22 </tr>
1<!-- Rows based table -->
3 <tr>
4 <th>Name:</th>
5 <td>John Doe</td>
6 <td>Jane Melan</td>
7 <td>Ben Foster</td>
8 </tr>
9 <tr>
10 <th>Age:</th>
11 <td>35</td>
12 <td>38</td>
13 <td>29</td>
14 </tr>
15 <tr>
16 <th>Height:</th>
17 <td>1.82</td>
18 <td>1.7</td>
19 <td>1.8</td>
20 </tr>

This will, however, only work with simple tables with a small amount of data. Firstly, because there is inconsistency in how different screen reader/browser combinations are interpreting and reading the column/row context. Secondly, tables, where the data is two dimensional (meaning the column and row context are essential to evaluate the data in each table-cell), require that the context is explicit so that screen readers will simultaneously associate a cell to a row and a column.
You can define the scope of each table header by using the “scope” attribute. To make an explicit column scope, use scope=”col” or scope=”row” for a row scope.

2 <tr>
3 <th scope="col">Name</th>
4 <th scope="col">Age</th>
5 <th scope="col">Height</th>
6 </tr>
7 <tr>
8 <th scope="row">John Doe</th>
9 <td>35</td>
10 <td>1.82</td>
11 </tr>
12 <tr>
13 <th scope="row">Jane Melan</th>
14 <td>38</td>
15 <td>1.7</td>
16 </tr>
17 <tr>
18 <th scope="row">Ben Foster</th>
19 <td>29</td>
20 <td>1.8</td>
21 </tr>

When navigating the table above using a screen reader, it will read the person's name when skipping between rows before reading the cell's content. If, for example, one skips into the last row on the "Age" column, screen readers will read it like this: "Ben Foster, 29". Note that the screen reader does not read the column header because we have changed only the row's context, and we are still in the same column's scope. If we were to navigate from the "Age" column to the height column (on the same row), the screen reader will read "Height, 1.8," ignoring the name of the row. On more complex tables where you have more than one level of context, like the table below, you may need to group a few columns under one scope.

Hot DogsHamburgers

To group columns under the scope of on <th>, use scope=”colgroup” like with the col and row scopes, the screen reader will read the header only when navigating into its scope. You can also group rows by using scope=”rowgroup”.

2 <tr>
3 <td rowspan="2"></td>
4 <th colspan="2" scope="colgroup">Hot Dogs</th>
5 <th colspan="2" scope="colgroup">Hamburgers</th>
6 </tr>
7 <tr>
8 <th scope="col">Prepared</th>
9 <th scope="col">Sold</th>
10 <th scope="col">Prepared</th>
11 <th scope="col">Sold</th>
12 </tr>
13 <tr>
14 <th scope="row">Saturday</th>
15 <td>150</td>
16 <td>136</td>
17 <td>300</td>
18 <td>279</td>
19 </tr>
20 <tr>
21 <th scope="row">Sunday</th>
22 <td>200</td>
23 <td>183</td>
24 <td>350</td>
25 <td>327</td>
26 </tr>

It is also possible to explicitly associate a table-cell with one or more headers. This method can be beneficial on very complex tables with multi-level headers and a lot of data. However, it is better to, if possible, avoid these kinds of complex tables. Even when cells are explicitly associated with headers, they are still harder to track and less clear to screen reader users. First, to associate a table-cell to a specific header, the <th> element must have an "id" attribute. Now you can associate the table-cell with its header by adding to the <td> element a "headers" attribute and assign to it the <th>'s id.

2 <th id="user-name">Name</th>
3 <!-- [...] -->
5<!-- [...] -->
7 <td headers="user-name">Marie Beauchamp</td>
8 <!-- [...] -->

You can associate more than one header to a cell by adding more ids to the headers attribute separated by spaces.

Accessible name

Naming tables is not a mandatory accessibility requirement. It can, however, improve the UX of screen reader users. When the table is titled, screen readers announce its name and thus put it into context. You can name tables using the <caption> element or one of the WAI-ARIA’s labeling attributes (e.g., aria-label, aria-labelledby).

2 <caption>A description of the table (e.g., Vegetables By Color)</caption>
4 <!-- The table content -->

Learn more about accessible names here

Color contrast

The text color on table-cells must have a sufficient contrast from its background. The WCAG's AA conformance level requires that the color contrast ratio of texts and their backgrounds is at least 4.5:1 Where the font size is larger than 24 CSS pixels (e.g., font-size: 24px), or where the text is bold and larger than 18.5 CSS pixels, a contrast ratio of 3:1 is sufficient. Learn more about color contrast here, or test your contrast ratios using the WCAG Contrast Checker.

Equivalent WAI-ARIA Role

role=”table”, role=”cell”, role=”columnheader”, role=”row”, role=”rowheader”

Required properties/attributes/context

See here an example of the WAI-ARIA’s table design pattern.

Additional Reading

W3C's Web Accessibility Tutorials - Tables