All non-empty <td> elements in a <table> larger than 3 by 3 must have an associated table header

Rule Description

Data table markup can be tedious and confusing. It is important that it is done semantically and with the correct header structure. Screen readers have a number of features to make table navigation easier, but tables must be marked up accurately for these features to work properly.

The Algorithm, in Simple Terms

Checks that data tables are marked up semantically and have the correct header structure

Why this is Important

Screen readers have a specific way of announcing tables. When tables are not properly marked up, this creates the opportunity for confusing or inaccurate screen reader output.

When tables are not marked up semantically and do not have the correct header structure, screen reader users cannot properly perceive visually the relationships between the cells and their contents.

Compliance Data & Impact

User Impact: Serious
Disabilities Affected:
  • Blindness
  • Deafblindness
Severity: Serious
Issue Type:
  • WCAG 2.0 (A): MUST
  • Section 508: MUST
  • Deque Experimental
  • WCAG 2.1 (A): MUST
WCAG Success Criteria:
  • 1.3.1 Info and Relationships
Section 508 Guidelines:
  • 1194.22 (g) Row and column headers for data tables

How to Fix the Problem

To fix the problem, ensure that each non-empty data cell in a large table has one or more table headers. All table data cells (<td>) must have a table header to ensure screen reader users can make sense of tabular data.

Note: A table is considered large if it is 3 or more cells wide and 3 or more cells high.

Example: Simple Data Table with <th scope="col"> and <th scope="row">

To markup a table cell as a header cell, change the <td> to a <th>. You will see that doing this to our example table causes the top row to have bolded, centered text.

Greensprings Running Club Personal Bests
Name 1 mile 5 km 10 km
Mary 8:32 28:04 1:01:16
Betsy 7:43 26:47 55:38
Matt 7:55 27:29 57:04
Todd 7:01 24:21 50:35


<table class="data">
Greensprings Running Club Personal Bests
<th scope="col">Name</th>
<th scope="col">1 mile</th>
<th scope="col">5 km</th>
<th scope="col">10 km</th>
<th scope="row">Mary</th>
<th scope="row">Betsy</th>
<th scope="row">Matt</th>
<th scope="row">Todd</th>

Note: the visual aspects of table borders, fonts, margins, backgrounds, etc. can be defined using CSS.

Example: Complex table with id + headers

Complex tables benefit from the id + headers method of associating header cell with data cells. This method is time consuming, as every cell must be marked up with an identification of the row and column of each cell.

Where possible, an easier option may be to plan your data presentation in such as way that you can break up a complex table into a series of simpler tables. These tables may also be more usable for the general audience.

In the example below, scope attributes have been replaced with id attributes on the headers. All of the data cells contain a headers attribute. The headers attribute can take a list of id values, each separated by a space, for each of the relevant headers. For instance, the second cell in the second row has a headers value of "mary 1m" indicating that this cell is related to two headers: the row header cell for "mary" and the column header cell for "1m".

Example 2 (column group headers):
  Females Males
Mary Betsy Matt Todd
1 mile 8:32 7:43 7:55 7:01
5 km 28:04 26:47 27:29 24:21
10 km 1:01:16 55:38 57:04 50:35


<table class="data complex" border="1">
Example 2 (column group headers): 
<td rowspan="2"><span class="offscreen">empty</span></td>
<th colspan="2" id="females2">Females</th>
<th colspan="2" id="males2">Males</th>
<th width="40" id="mary2">Mary</th>
<th width="35" id="betsy2">Betsy</th>
<th width="42" id="matt2">Matt</th>
<th width="42" id="todd2">Todd</th>
<th width="39" id="mile1_2">1 mile</th>
<td headers="females2 mary2 mile1_2">8:32</td>
<td headers="females2 betsy2 mile1_2">7:43</td>
<td headers="males2 matt2 mile1_2">7:55</td>
<td headers="males2 todd2 mile1_2">7:01</td>
<th id="km5_2">5 km</th>
<td headers="females2 mary2 km5_2">28:04</td>
<td headers="females2 betsy2 km5_2">26:47</td>
<td headers="males2 matt2 km5_2">27:29</td>
<td headers="males2 todd2 km5_2">24:21</td>
<th id="km10_2">10 km</th>
<td headers="females2 mary2 km10_2">1:01:16</td>
<td headers="females2 betsy2 km10_2">55:38</td>
<td headers="males2 matt2 km10_2">57:04</td>
<td headers="males2 todd2 km10_2">50:35</td>

This method creates an explicit association between the data cells and header cells. Though tedious to mark up by hand, this approach is relatively easy to program with a server-side scripting language (PHP, .net, JSP, Python, etc.) for tables of data from a database.

Note: Old Versions of VoiceOver did Not Support the id + headers Method

Up until Mac OSX 10.10.2, VoiceOver did not support the ability to read table headers with the id + headers method. Some versions even read the wrong headers with the data cells. Fortunately, the current version of VoiceOver does read the data and header associations correctly.

Related Deque University Course Pages

Related Deque Code Library Examples

Other Related Resources

Additional Information

Relevant Technologies:
  • HTML 4
  • HTML5
Relevant Custom Widget Type(s):
  • Table - Sortable
  • Table - Responsive
Test Reliability: Automated testing is possible, with high accuracy
Rule ID: td-has-header