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

Rule ID: td-has-header
User Impact: Critical
WCAG: 1.3.1

Compliance Data & Impact

User Impact

Disabilities Affected

  • Blind
  • Deafblind

Requirement(s)

  • Section 508: MUST

WCAG Success Criteria

  • 1.3.1 Info and Relationships

Section 508 Guidelines

  • 1194.22 (g) Row and column headers for data tables

Rule Description

Data table markup can be tedious and confusing. Tables must be marked up done semantically and with the correct header structure. Screen readers have features to ease table navigation, but tables must be marked up accurately for these features to work correctly.

Why it Matters

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 correctly perceive the relationships between the cells and their contents visually.

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

HTML Code

<table class="data">
<caption>
Greensprings Running Club Personal Bests
</caption>
  <thead>
    <tr>
     <th scope="col">Name</th>
      <th scope="col">1 mile</th>
      <th scope="col">5 km</th>
      <th scope="col">10 km</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Mary</th>
      <td>8:32</td>
      <td>28:04</td>
      <td>1:01:16</td>
    </tr>
    <tr>
      <th scope="row">Betsy</th>
      <td>7:43</td>
      <td>26:47</td>
      <td>55:38</td>
    </tr>
    <tr>
      <th scope="row">Matt</th>
      <td>7:55</td>
      <td>27:29</td>
      <td>57:04</td>
    </tr>
    <tr>
      <th scope="row">Todd</th>
      <td>7:01</td>
      <td>24:21</td>
      <td>50:35</td>
    </tr>
  </tbody>
</table>

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 presented in such as way that you can break up a complex table into a series of simpler tables. These tables may also be more useful 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

HTML Code

<table class="data complex" border="1">
<caption>
Example 2 (column group headers): 
</caption>
<tr>
<td rowspan="2"><span class="offscreen">empty</span></td>
<th colspan="2" id="females2">Females</th>
<th colspan="2" id="males2">Males</th>
</tr>
<tr>
<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>
</tr>
<tr>
<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>
</tr>
<tr>
<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>
</tr>
<tr>
<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>
</tr>gt;

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, etcetera) 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.

The Algorithm (in simple terms)

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