Making HTML tables accessible can be a challenge. This article aims to make the process a little easier.
Data Tables
Structuring and displaying tabular data is what tables are designed to do. HTML has infrastructure in place to code extremely complex tables, cross referencing table data with the table headers that describe the data, adding metadata to similar types of table data, and much more.
However, most editing applications for the web are not geared towards properly coding tables, so often it helps to be able to troubleshoot and manually tweak the output of such editors to achieve good results. For this purpose, a knowledge of the principles of coding tables is essential.
This article assumes a basic knowledge of HTML.
The first level of table accessibility is 1) identifying table headers; and 2) making the relationship between table headers and table cells explicit.
Table headers
The starting point for all tables is the table element, which contains table rows (tr elements) and table cells (td elements).
A table might look as follows:
| Row 1, Cell 1 |
Row 1, Cell 2 |
Row 1, Cell 3 |
| Row 2, Cell 1 |
Row 2, Cell 2 |
Row 2, Cell 3 |
The above table is coded using a flat, non-hierarchical structure. The data in Row 1, Cell 1 is in no way related to any other data:
<table>
<tr>
<td>Row 1, Cell 1</td>
<td>Row 1, Cell 2</td>
<td>Row 1, Cell 3</td>
</tr>
<tr>
<td>Row 2, Cell 1</td>
<td>Row 2, Cell 2</td>
<td>Row 2, Cell 3</td>
</tr>
</table>
It is rarely the case that we have data that is completely unrelated, as the coding above suggests. For instance, it is often the case that each row or each column represents a specific type of data.
To label the data as a specific data type, we have the table header.
Table headers are represented by the HTML element th. Table headers are used similarly to table cells, in that they are contained in rows. A row may have both table cells and table headers.
A group of table headers can be contained in the thead element. Similarly, the main body of table data can reside in a tbody element.
<table>
<thead>
<tr>
<th>Fruits</th>
<th>Vegetables</th>
<th>Animals</th>
</tr>
</thead>
<tbody>
<tr>
<td>Orange</td>
<td>Leek</td>
<td>Giraffe</td>
</tr>
<tr>
<td>Pear</td>
<td>Potato</td>
<td>Penguin</td>
</tr>
</tbody>
</table>
The above code is rendered as follows:
| Fruits | Vegetables | Animals |
| Orange |
Leek |
Giraffe |
| Pear |
Potato |
Penguin |
Now we have identified the data type and given it a label. However, the label that we have applied in the th element is not yet related to the table data.
Relating table cells to table headers requires that certain cell and header attributes are used.
Table headers can take a number of attributes, the most important and relevant of which are:
title class id axis scope
Relationships between Table Headers and Table Cells
Making the relationship between table headers and table cells explicit can be accomplished in a few different ways. For simple data tables, defining a scope for the table header is sufficient.
Simple data table example
The data that you wish to present is conducive to the following tabular presentation:
- A single row of Column Headers
- Any number of Rows with data.
The visual appearance that is desired is as follows:
Caption
| Header 1 | Header 2 | Header 3 |
| Data cell 1 |
Data cell 2 |
Data cell 3 |
| Data cell 1 |
Data cell 2 |
Data cell 3 |
| Data cell 1 |
Data cell 2 |
Data cell 3 |
| Data cell 1 |
Data cell 2 |
Data cell 3 |
Such a table could be marked up using the headers attribute for the table cells to cross-reference the id attribute of the relevant column header.
<table>
<caption>Caption</caption>
<thead>
<tr>
<th id="h1">Header 1</th>
<th id="h2">Header 2</th>
<th id="h3">Header 3</th>
</tr>
</thead>
<tbody>
<tr>
<td headers="h1">Data cell 1</td>
<td headers="h2">Data cell 2</td>
<td headers="h3">Data cell 3</td>
</tr>
<tr>
<td headers="h1">Data cell 1</td>
<td headers="h2">Data cell 2</td>
<td headers="h3">Data cell 3</td>
</tr>
<tr>
<td headers="h1">Data cell 1</td>
<td headers="h2">Data cell 2</td>
<td headers="h3">Data cell 3</td>
</tr>
<tr>
<td headers="h1">Data cell 1</td>
<td headers="h2">Data cell 2</td>
<td headers="h3">Data cell 3</td>
</tr>
</tbody>
</table>
In the example above, the table cells with the content Data cell 1 with the content Header 1 all have a headers attribute with the value h1. This value is used to cross-reference the id attribute of the table header to locate the header that applies.
Option 2: Using the colgroup element and scope attribute on the headers
Another option is to mark up the page using the scope attribute on the column headers. The scope attribute identifies the table header as either a column or a row header, and binds the information contained in data cells in the same row or column to that header.
<table>
<caption>Caption</caption>
<thead>
<tr>
<th id="h1" scope="col">Header 1</th>
<th id="h2" scope="col">Header 2</th>
<th id="h3" scope="col">Header 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data cell 1</td>
<td>Data cell 2</td>
<td>Data cell 3</td>
</tr>
<tr>
<td>Data cell 1</td>
<td>Data cell 2</td>
<td>Data cell 3</td>
</tr>
<tr>
<td>Data cell 1</td>
<td>Data cell 2</td>
<td>Data cell 3</td>
</tr>
<tr>
<td>Data cell 1</td>
<td>Data cell 2</td>
<td>Data cell 3</td>
</tr>
</tbody>
</table>
Option 3: Using the axis attribute on the data cells and headers
According to W3C, the axis attribute is used […] to place a cell into conceptual categories that can be considered to form axes in an n-dimensional space.
This allows you to add a level of conceptual information to every data item, regardless of whether this data is contained in a cell or a header.
Axes are added as comma-separated attributes
In order to demonstrate this, consider the following example. All cells and headers that have an axis defined are marked with an asterisk (*).
Read and unread mail on your Mail servers.
| Read * | Unread * |
| Buddies * | Unknown * | Buddies * | Unknown * |
| mailserver1 * |
12 |
13242 |
567 |
2345 |
| mailserver2 * |
12 |
15 |
13245 |
45 |
| mailserver3 * |
34321 |
3421 |
9786 |
345 |
|---|
Notice that the headers in the first column (mailserver1, mailserver2 and mailserver 3) are conceptually related: they are all the source of the emails.
Similarly the first and second header rows are each interrelated: The first row describes a Status (Read or Unread); and the second row describes your relationship with the sender.
This information could have been marked up using table headers with a column scope for the first column, and headers with row scope for the two header rows. However, using the axis attribute, we are able to add that information without cluttering the mark-up with superfluous th elements.
Additionally, this table is relatively complex, so a summary attribute should be added to the table element that briefly describes what data will be presented in the table. This enables non-visual user agents to present a synopsis of the table content akin to information that one would glean from scanning the table visually, without delving into the details of the table.
<table summary="Read and unread mail on three mail servers, split into two categories: senders that have been identified as Buddies, and unknown senders.">
<caption>Read and unread mail on your Mail servers.</caption>
<thead>
<tr>
<td></td>
<th colspan="2" id="read" axis="status">Read</th>
<th colspan="2" id="unread" axis="status">Unread</th>
</tr>
<tr>
<td></td>
<th id="r_fr" axis="relationship">Buddies</th>
<th id="r_un" axis="relationship">Unknown</th>
<th id="u_fr" axis="relationship">Buddies</th>
<th id="u_un" axis="relationship">Unknown</th>
</tr>
</thead>
<tbody>
<tr>
<th id="server" axis="source">mailserver1</th>
<td headers="read r_fr server">12</td>
<td headers="read r_un server">13242</td>
<td headers="unread u_fr server">567</td>
<td headers="unread u_un server">2345</td>
</tr>
<tr>
<th id="server2" axis="source">mailserver2</th>
<td headers="read r_fr server2">12</td>
<td headers="read r_un server2">15</td>
<td headers="unread u_fr server2">13245</td>
<td headers="unread u_un server2">45</td>
</tr>
<tr>
<th id="server3" axis="source">mailserver3</th>
<td headers="read r_fr server3">34321</td>
<td headers="read r_un server3">3421</td>
<td headers="unread u_fr server3">9786</td>
<td headers="unread u_un server3">345</td>
</tr>
</tbody>
</table>
Simple data table example 2
Caption
| Header 1 |
Data cell 1 |
Data cell 2 |
Data cell 3 |
| Header 2 |
Data cell 1 |
Data cell 2 |
Data cell 3 |
| Header 3 |
Data cell 1 |
Data cell 2 |
Data cell 3 |
| Header 4 |
Data cell 1 |
Data cell 2 |
Data cell 3 |
|---|