Bring on the tables

Something that often seems to confuse people that are new to CSS-based layouts is the use of tables. I've seen plenty of cases where people interpret "avoid using tables for layout" as "don't use tables at all". It's important to remember that tables are still perfectly fine to use – if used correctly.

Yes, do your best to avoid using tables for layout, but for tabular data, tables are what you should use. I'd like to talk about how tables should be used when marking up tabular data. There's a lot more to tables in HTML and XHTML than just rows and cells. Much more. Especially if you want to make them accessible.

First a bit of background info. The "avoid using tables for layout" blurb can be found in Introduction to tables in the HTML 4.01 Specification:

Tables should not be used purely as a means to layout document content as this may present problems when rendering to non-visual media. Additionally, when used with graphics, these tables may force users to scroll horizontally to view a table designed on a system with a larger display. To minimize these problems, authors should use style sheets to control layout rather than tables.

I'd say that makes it pretty clear, although the word used is should, not must, so there is some flexibility in the specification.

But this article is not about using tables for layout or not. It's about using tables for their original purpose: marking up tabular data.

When tables are used to mark up actual data, they aren't just a layout grid. Sighted people can get a feel for the relationship between header and data cells by looking at the layout and visual presentation of the table. Blind or severly vision impaired people can't do that. For a table to be accessible to people using a screen reader or some other non-visual user agent, it needs to tell the user agent how the information it contains is related.

Fortunately, HTML provides plenty of elements and attributes for that. Less fortunate is the fact that it can be pretty difficult to understand how to use some of these accessibility features. In this article, I'll try to explain how most of them can be used.

Table headers, <th>

Let's start with a very simple table that only has a single row of headers, each defining the data in a column. Marked up the old-fashioned way, with just table rows and cells, the markup would be this:

  1. <table>
  2. <tr>
  3. <td>Company</td>
  4. <td>Employees</td>
  5. <td>Founded</td>
  6. </tr>
  7. <tr>
  8. <td>ACME Inc</td>
  9. <td>1000</td>
  10. <td>1947</td>
  11. </tr>
  12. <tr>
  13. <td>XYZ Corp</td>
  14. <td>2000</td>
  15. <td>1973</td>
  16. </tr>
  17. </table>

With no borders or styling, the table will look like this in most browsers:

Company Employees Founded
ACME Inc 1000 1947
XYZ Corp 2000 1973

By styling the table with a bit of CSS, you can make the headers more obvious for those using a graphical browser:

Company Employees Founded
ACME Inc 1000 1947
XYZ Corp 2000 1973

For a sighted person, it's now quick and easy to make the connection between headers and the data cells they describe. Someone using a screen reader, on the other hand, would hear something like Company Employees Founded ACME Inc 1000 1947 XYZ Corp 2000 1073. Not very easy to make any sense out of.

The first – and easiest – step towards making this table more accessible is to mark up the headers properly. It's very simple: just use the <th> (table header) element instead of <td> (table data) for the header cells:

  1. <table>
  2. <tr>
  3. <th>Company</th>
  4. <th>Employees</th>
  5. <th>Founded</th>
  6. </tr>
  7. <tr>
  8. <td>ACME Inc</td>
  9. <td>1000</td>
  10. <td>1947</td>
  11. </tr>
  12. <tr>
  13. <td>XYZ Corp</td>
  14. <td>2000</td>
  15. <td>1973</td>
  16. </tr>
  17. </table>
Company Employees Founded
ACME Inc 1000 1947
XYZ Corp 2000 1973

For this simple table, that is enough information for a screen reader to be able to let the user know which header each data cell is related to. A screen reader might say Company: ACME Inc. Employees: 1000. Founded: 1947., and so on for each row. Much better.

Table captions: <caption>

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. I'll leave it to you to experiment with this.

When used, the <caption> element must be the very first thing after the opening <table> tag:

  1. <table>
  2. <caption>Table 1: Company data</caption>
  3. <tr>
  4. <th>Company</th>
  5. <th>Employees</th>
  6. <th>Founded</th>
  7. </tr>
  8. <tr>
  9. <td>ACME Inc</td>
  10. <td>1000</td>
  11. <td>1947</td>
  12. </tr>
  13. <tr>
  14. <td>XYZ Corp</td>
  15. <td>2000</td>
  16. <td>1973</td>
  17. </tr>
  18. </table>
Table 1: Company data
Company Employees Founded
ACME Inc 1000 1947
XYZ Corp 2000 1973

You can of course use CSS to style the caption if you wish. However, be aware that it can be a little tricky to style it consistently across browsers. I'll leave that as another exercise for you.

Explaining the table: the summary attribute

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 we add a summary attribute to the table element. This way you can provide a more detailed description of the table than is suitable for the <caption> element.

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. Don't overdo it though, and use the summary attribute only when necessary, i.e. for more complex tables where a summary will make it easier for someone using a screen reader to understand the contents of the table.

  1. <table summary="The number of employees and the foundation year of some imaginary companies.">
  2. <caption>Table 1: Company data</caption>
  3. <tr>
  4. <th>Company</th>
  5. <th>Employees</th>
  6. <th>Founded</th>
  7. </tr>
  8. <tr>
  9. <td>ACME Inc</td>
  10. <td>1000</td>
  11. <td>1947</td>
  12. </tr>
  13. <tr>
  14. <td>XYZ Corp</td>
  15. <td>2000</td>
  16. <td>1973</td>
  17. </tr>
  18. </table>

Shortening the headers: the abbr attribute

When a screen reader encounters a table, it can announce the associated header (or headers) before each data cell. If you have long headers, hearing them repeated over and over can get tedious. By using the abbr attribute to provide an abbreviated version of any long headers, you give screen readers something that they can use instead of the text in the header itself. Using the abbr attribute is optional, and most of the time your headers will (and probably should) be pretty short anyway.

If the example table is modified slightly, to make the headers longer, the abbr attribute could be used like this:

  1. <table summary="The number of employees and the foundation year of some imaginary companies.">
  2. <caption>Table 1: Company data</caption>
  3. <tr>
  4. <th abbr="Company">Company Name</th>
  5. <th abbr="Employees">Number of Employees</th>
  6. <th abbr="Founded">Foundation Year</th>
  7. </tr>
  8. <tr>
  9. <td>ACME Inc</td>
  10. <td>1000</td>
  11. <td>1947</td>
  12. </tr>
  13. <tr>
  14. <td>XYZ Corp</td>
  15. <td>2000</td>
  16. <td>1973</td>
  17. </tr>
  18. </table>
Table 1: Company data
Company Name Number of Employees Foundation Year
ACME Inc 1000 1947
XYZ Corp 2000 1973

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:

  1. <table summary="The number of employees and the foundation year of some imaginary companies.">
  2. <caption>Table 1: Company data</caption>
  3. <tr>
  4. <td></td>
  5. <th>Employees</th>
  6. <th>Founded</th>
  7. </tr>
  8. <tr>
  9. <th>ACME Inc</th>
  10. <td>1000</td>
  11. <td>1947</td>
  12. </tr>
  13. <tr>
  14. <th>XYZ Corp</th>
  15. <td>2000</td>
  16. <td>1973</td>
  17. </tr>
  18. </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:

  1. <table summary="The number of employees and the foundation year of some imaginary companies.">
  2. <caption>Table 1: Company data</caption>
  3. <tr>
  4. <td></td>
  5. <th scope="col">Employees</th>
  6. <th scope="col">Founded</th>
  7. </tr>
  8. <tr>
  9. <th scope="row">ACME Inc</th>
  10. <td>1000</td>
  11. <td>1947</td>
  12. </tr>
  13. <tr>
  14. <th scope="row">XYZ Corp</th>
  15. <td>2000</td>
  16. <td>1973</td>
  17. </tr>
  18. </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. I'll get back to those in a bit.

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:

  1. <table summary="The number of employees and the foundation year of some imaginary companies.">
  2. <caption>Table 1: Company data</caption>
  3. <tr>
  4. <th scope="col">Company</th>
  5. <th scope="col">Employees</th>
  6. <th scope="col">Founded</th>
  7. </tr>
  8. <tr>
  9. <td scope="row">ACME Inc</td>
  10. <td>1000</td>
  11. <td>1947</td>
  12. </tr>
  13. <tr>
  14. <td scope="row">XYZ Corp</td>
  15. <td>2000</td>
  16. <td>1973</td>
  17. </tr>
  18. </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:

  1. td[scope] {
  2. font-weight:bold;
  3. }

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:

  1. <table class="extbl" summary="The number of employees and the foundation year of some imaginary companies.">
  2. <caption>Table 1: Company data</caption>
  3. <tr>
  4. <td rowspan="2"></td>
  5. <th id="employees" colspan="2">Employees</th>
  6. <th id="founded" rowspan="2">Founded</th>
  7. </tr>
  8. <tr>
  9. <th id="men">Men</th>
  10. <th id="women">Women</th>
  11. </tr>
  12. <tr>
  13. <th id="acme">ACME Inc</th>
  14. <td headers="acme employees men">700</td>
  15. <td headers="acme employees women">300</td>
  16. <td headers="acme founded">1947</td>
  17. </tr>
  18. <tr>
  19. <th id="xyz">XYZ Corp</th>
  20. <td headers="xyz employees men">1200</td>
  21. <td headers="xyz employees women">800</td>
  22. <td headers="xyz founded">1973</td>
  23. </tr>
  24. </table>
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.

That's all I'm going to say about columns and column groups here. For more detailed information, see the links in the section "What I didn't tell you".

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.

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:

  1. <table>
  2. <thead>
  3. <tr></tr>
  4. … more rows for the table head
  5. </thead>
  6. <tfoot>
  7. <tr></tr>
  8. … more rows for the table foot
  9. </tfoot>
  10. <tbody>
  11. <tr></tr>
  12. … more rows for the first table body
  13. </tbody>
  14. <tbody>
  15. <tr></tr>
  16. … more rows for the second table body
  17. </tbody>
  18. … more table bodies if necessary
  19. </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.

What I didn't tell you

There is even more to data tables than I have mentioned here. For example, I haven't mentioned the axis attribute at all (until now), and I did not describe the <colgroup> and <col> elements in great depth. Neither have I gone into formatting and styling or the border models. Also missing is an example of a really complex table.

For those of you looking for even more detailed information, here are links to some further reading:

Translations

This article has been translated into the following languages:

Posted on October 27, 2004 in Web Standards, Accessibility, (X)HTML