Tables and Accessibility

As much as possible tables should be used for tabular content as opposed to controlling layout. Layout should be defined in a style sheet. Additionally, data presented in a table should be prepared with accessibility requirements in mind, allowing screen readers to provide the same information that would be readily available to sighted users.

“Bring on the Tables” is copyrighted to Roger Johansson, 456 Berea Street. Reprinted with permission.

Headers

Table data is divided into table headers and table cells. Titles for rows or columns should use the <th> tag:

<table>
  <thead>
  <tr>
    <th>Company</th>
    <th>Employees</th>
  </tr>
  </thead>
</table>

Captioning

The <caption> element can be used to provide a short description of a table, much like an image caption. By default, most visual browsers render the caption element centered above the table. The CSS property caption-side can be used to change that, if necessary. Most browsers will only display the caption either above (top) or below (bottom) the table contents, while some will accept left or right as values.

<table> 
  <caption>Table 1: Company data</caption> 
    <tr> 
      <th>Company</th>

Summary

A sighted person can easily decide whether or not to study a table in detail. A quick glance will tell how large the table is and roughly what it contains. A person using a screen reader can’t do that unless a summary attribute is added to the table element. This way a more detailed description of the table than is suitable for the <caption> element can be provided.

The contents of the summary attribute will not be rendered by visual browsers, so make the description long enough for anyone hearing it to understand what the table is about.

<table summary="A list of employees and number of years worked."> 
  <caption>Table 1: Employment Record</caption> 
    <tr> 
      <th>Employee</th>
      <th>Year</th>

Linking headers to data–the scope, id and headers attributes

Many tables are far more complex than a simple two column expression of data.

The scope attribute defines whether a header cell provides header information for a column or a row:

  • col: header information for the column it is in
  • row: header information for the row it is in

Adding a scope attribute with the value col to the headers in the first row declares that they are headers for the data cells below them. Likewise, giving the headers that begin each row a scope with the value row makes them headers for the data cells to their right.

The scope attribute can take two more values:

  • colgroup: header information for the rest of the column group that contains it
  • rowgroup: header information for the rest of the row group that contains it

A column group is defined by the <colgroup> element. Row groups are defined by the <thead>, <tfoot> and <tbody> elements.

What if you want to keep the “Employee” header, and still have the employee names be row headers? That would make the cells containing the employee names provide both header and data information. In this case, <td> should be used together with the scope attribute:

<table summary="A list of divisional quarterly budget"> 
  <caption>Table 1: Budget Report</caption> 
    <tr> 
      <th scope="col">Division</th>
      <th scope="col">Current Qtr</th>
      <th scope="col">Previous Qtr</th>
    </tr>
    <tr> 
      <td scope="row">Sales</td> 
      <td>$250,000</td> 
      <td>$232,000</td> 
    </tr> 
    <tr> 
      <td scope="row">Marketing</td> 
      <td>$651,000</td> 
      <td>$479,000</td> 
    </tr> 
</table>

Visual browsers won’t display the division names as headers by default, so a bit of CSS is required. Example:

td[scope] {
  font-weight: bold;
  }

Note that this rule uses an attribute selector, which Internet Explorer does not support. A workaround for that would be to add a class to any data cells that should be styled as a header.

Another technique for connecting a table’s data cells with their appropriate header involves giving each header a unique id. A headers attribute is then added to each data cell. This attribute contains a list, separated by spaces, of the id of every header cell that applies to that data cell. This technique is more complicated, and should only be used when there are data cells that need to be linked to more than two header cells, and the scope attribute is insufficient, as in a very complex or irregular table.

<table class="extbl" summary="The number of employees and the foundation year of some imaginary companies.">
<caption>Table 1: Company data</caption>
  <tr>
    <td rowspan="2"></td>
    <th id="employees" colspan="2">Employees</th>
    <th id="founded" rowspan="2">Founded</th>
  </tr>
  <tr>
    <th id="men">Men</th>
    <th id="women">Women</th>
  </tr>
  <tr>
    <th id="acme">ACME Inc</th>
    <td headers="acme employees men">700</td>
    <td headers="acme employees women">300</td>
    <td headers="acme founded">1947</td>
  </tr>
  <tr>
    <th id="xyz">XYZ Corp</th>
    <td headers="xyz employees men">1200</td>
    <td headers="xyz employees women">800</td>
    <td headers="xyz founded">1973</td>
  </tr>
</table>

 

Table 1: Company data

Employees Founded
Men Women
ACME Inc 700 300 1947
XYZ Corp 1200 800 1973

To style individual areas based on the id:

th#xyz { background-color: #9C9; text-align: right;}

A screen reader could then read the full length headers for the first row of data, and then use the abbreviation for the remaining rows.

Considering how hard it can be to make data tables fit a layout, I’d say it’s more common to have a need for the opposite: to make the headers as short as possible, or even abbreviated, and use the title attribute or the <abbr> element to provide a longer explanation.

Linking headers to data: the scope, id and headers attributes

Many tables are more complex than the example table I’ve been using so far. I’ll make it a little more complex by removing the “Company” header and changing the data cells in the first column into header cells:

<table summary="The number of employees and the foundation year of some imaginary companies.">
 <caption>Table 1: Company data</caption>
  <tr>
   <td></td>
   <th>Employees</th>
   <th>Founded</th>
  </tr>
  <tr>
   <th>ACME Inc</th>
    <td>1000</td>
    <td>1947</td>
  </tr>
  <tr>
   <th>XYZ Corp</th>
    <td>2000</td>
    <td>1973</td>
 </tr>
</table>

 

Table 1: Company data

Employees Founded
ACME Inc 1000 1947
XYZ Corp 2000 1973

In this table, each data cell has two headers. The simplest method, markup-wise, of making sure that a non-visual browser can make sense of this table is to add a scope attribute to all header cells:

<table summary="The number of employees and the foundation year of some imaginary companies.">
<caption>Table 1: Company data</caption>
<tr>
<td></td>
<th scope="col">Employees</th>
<th scope="col">Founded</th>
</tr>
<tr>
<th scope="row">ACME Inc</th>
<td>1000</td>
<td>1947</td>
</tr>
<tr>
<th scope="row">XYZ Corp</th>
<td>2000</td>
<td>1973</td>
</tr>
</table>

The scope attribute defines whether a header cell provides header information for a column or a row:

  • col: header information for the column it is in
  • row: header information for the row it is in

Adding a scope attribute with the value col to the headers in the first row declares that they are headers for the data cells below them. Likewise, giving the headers that begin each row a scope with the value row makes them headers for the data cells to their right.

The scope attribute can take two more values:

  • colgroup: header information for the rest of the column group that contains it
  • rowgroup: header information for the rest of the row group that contains it

A column group is defined by the <colgroup> element. Row groups are defined by the <thead>, <tfoot> and <tbody> elements.

What if you want to keep the “Company” header, and still have the company names be row headers? That would make the cells containing the company names provide both header and data information. In this case, <td> should be used together with the scope attribute:</td>

<table summary="The number of employees and the foundation year of some imaginary companies.">
 <caption>Table 1: Company data</caption>
 <tr>
  <th scope="col">Company</th>
  <th scope="col">Employees</th>
  <th scope="col">Founded</th>
 </tr>
 <tr>
  <td scope="row">ACME Inc</td>
  <td>1000</td>
  <td>1947</td>
 </tr>
 <tr>
  <td scope="row">XYZ Corp</td>
  <td>2000</td>
  <td>1973</td>
 </tr>
</table>

This way, visual browsers won’t display the company names as headers by default, so a bit of CSS is needed to fix that. For this example, I used the following CSS:

td[scope] {
font-weight:bold;
}

Note that this rule uses an attribute selector, which Internet Explorer does not support. A workaround for that would be to add a class to any data cells that should be styled as a header.

Table 1: Company data

Company Employees Founded
ACME Inc 1000 1947
XYZ Corp 2000 1973

Another technique for connecting a table’s data cells with their appropriate header involves giving each header a unique id. A headers attribute is then added to each data cell. This attribute contains a list, separated by spaces, of the id of every header cell that applies to that data cell. This technique is more complicated, and should only be used when there are data cells that need to be linked to more than two header cells, and the scope attribute is insufficient, as in a very complex or irregular table.

To illustrate this, I’ve changed the table to show the number of employees of each sex the companies have:

Table 1: Company data Employees Founded Men Women ACME Inc 700 300 1947 XYZ Corp 1200 800 1973

As you can tell, this method quickly gets really complicated, so if it is possible, use the scope attribute instead.

Spanning rows and columns

In the old, tables-for-layout days, the attributes rowspan and colspan were often used to make table cells span several rows or columns in order to put all of the neatly sliced images back together. Those attributes are still around – there is no way to use CSS to specify spanning. If you think about it, it’s quite logical: row and column spans are part of a table’s structure, not its presentation.

Columns and column groups: <col> and <colgroup>

HTML provides the <colgroup> and <col> elements for grouping related table columns. This allows (in some browsers) the use of CSS to style columns independently. Column groups can also be used by the scope attribute to specify that a cell contains header information for the rest of the column group that contains it.

Row groups: <thead>, <tfoot>, and <tbody>

Table rows can be grouped into a table head (<thead>), a table foot (<tfoot>), and one or more table body (<tbody>) sections. Each row group must contain one or more table rows.</tbody>

If a table has a head section, it must appear before the table foot and body sections. A table foot section must appear before the body section(s). If no head or foot section is used, the <tbody> element is not required (but not forbidden either, so add it if you like). The structure of a table that has row groups looks like this:</tbody>.

<table class="extbl" summary="The number of employees and the foundation year of some imaginary companies.">
 <caption>Table 1: Company data</caption>
  <tr>
   <td rowspan="2"></td>
   <th id="employees" colspan="2">Employees</th>
   <th id="founded" rowspan="2">Founded</th>
  </tr>
  <tr>
   <th id="men">Men</th>
   <th id="women">Women</th>
  </tr>
  <tr>
   <th id="acme">ACME Inc</th>
   <td headers="acme employees men">700</td>
   <td headers="acme employees women">300</td>
   <td headers="acme founded">1947</td>
  </tr>
  <tr>
   <th id="xyz">XYZ Corp</th>
   <td headers="xyz employees men">1200</td>
   <td headers="xyz employees women">800</td>
   <td headers="xyz founded">1973</td>
  </tr>
</table>

Row grouping can be useful for several reasons:

  • It makes it easy to style the head, foot, and body sections of a table independently of each other, without having to add classes to any elements.
  • When printing long tables, some browsers (like those based on Mozilla) will repeat the information in the head and foot sections on every printed page, making it easier to read the printed table.
  • Separating the head and foot from the body also makes it possible for browsers to support scrolling of the table body only.

For data tables only

Everything described here is related to the use of HTML tables to structure and present data. If you use tables for layout, neither of the techniques described here should be used. No summary attribute, no headers, no <caption>, nothing. Just a plain, old-fashioned layout table, consisting of no other elements than <table>, <tr>, and <td>. Otherwise you will risk confusing users of non-visual user agents even more.

The benefits

It may look like a lot of work to create accessible data tables in HTML. For complex tables, it is. Sometimes to the point where it gets almost impossible to do by hand. For simple tables though, using header cells with a scope attribute is quick and easy.

It’s obvious that people using screen readers or other assistive technology benefit from tables that use the available accessibility features. Trying to make sense of a large and complex table by listening to it can still be very difficult, so if at all possible, simplify the table.

Less obvious is that designers and users of graphical browsers also benefit: an accessible table has plenty of structural hooks to apply CSS to, and good styling can make the table more usable for everybody.

Scroll Up