HTML, CSS and tables - the beauty of data

Preface

HTML tables are still many a discussed topic on various mailing lists, forums and chat channels. Many people have realised that they are not the most clever method of defining the layout of a page.

However, it is still quite common not to use the latest markup available to make them more usable and accessible, and many inexperienced CSS users moan about styling issues. This article shows the evolution of HTML tables in web development and provides examples of up-to-date markup and styling practices.

What we use(d) tables for

Web development would not be the same if HTML hadn't had a table tag. In the days before CSS, they were the only way to create multi column layouts, visually appealing menus and turn those pixel-perfect print-inspired screen designs into something people can see on the web. We got used to tables being a necessity, we learnt how to force browsers to display them correctly and generally thought we got that "web development" thing licked.

Tables used TABLE, TR, and TD elements and we were able to control them with the attributes align, valign, background, bgcolor, rowspan, colspan, cellpadding, cellspacing and border. We used all that - and transparent graphics - to create the look we were trying to achieve. Changing this look, or even re-arranging the whole page layout was a royal pain in parts unmentioned here, but we saw that as a necessary evil. Things turned bad when newer browsers came out, and our markup monsters started to fail. We stood there, blinking, and wondered what went wrong. Well, we used tables as visuals constructs, and we forgot what a table really is, that's what.

What is a table?

When it comes down to the basics, a table is data. It is a construct that shows relations between data in a fashion that is easy to grasp and understand. One look is enough and we know what is what and what it connects to:

Flight Number: From: To: Departure: Arrival:
BA 3451 Heathrow Nuremberg 19:20 19:50
BA 1254 Luton Alicante 19:40 20:50
LH 331 Heathrow Hamburg 20:00 20:20

This works swimmingly for anybody who has the table right in front of his nose and can see it. It is different though, when we cannot see the table.

Where tables fail and what we can do about it

Pick up the phone, and read out the above table to someone. You will find yourself repeating the above information over and over again. "Flight BA 3451 flies from Heathrow to Nuremberg at 19:20 and arrives at 19:50 and 1254 from Luton to Alicante at 19:40 and arrives at 20:50. You got that? OK. Flight LH 331 flies from Heathrow to Hamburg at 8pm and arrives at 8.20 local time."

For a short table like this, you can leave out some of the labels, but for long tables, you will end up with a lot to explain. For the person on the other side of the phoneline you can explain all that, web users who use text browsers or screen readers don't get that information. All they get is:

Flight Number: From: To: Departure: Arrival: BA 3451 Heathrow Nuremberg 19:20 19:50 BA 1254 Luton Alicante 19:40 20:50 LH 331 Heathrow Hamburg 20:00 20:20

W3C to the rescue

To allow assistive technology to give users this kind of information, the W3C extended the original HTML table specification by adding some more structural elements and attributes: THEAD, TFOOT, TH, TBODY, CAPTION, summary, axis, headers and scope. Serialization of tables is a vast subject and covered in many extensive tutorials. Here's a quick introduction how we can make our tables more accessible for users depending on assistive technology:

You start with table headers. These TH elements define a cell that gives information about the cells connected to it. In our case, these are the ones in the first row and the ones containing the flight number:

	<table>
	  <tr>
	    <th>Flight Number:</th>  
	    <th>From:</th>  
	    <th>To:</th>  
	    <th>Departure:</th>  
	    <th>Arrival:</th>  
	  </tr>
	  <tr>
	    <th>BA 3451</th>
	    <td>Heathrow</td>
	    <td>Nuremberg</td>
	    <td>19:20</td>
	    <td>19:50</td>
	  </tr>
	  <tr>
	    <th>BA 1254</th>
	    <td>Luton</td>
	    <td>Alicante</td>
	    <td>19:40</td>
	    <td>20:50</td>
	  </tr>
	  <tr>
	    <th>LH 331</th>
	    <td>Heathrow</td>
	    <td>Hamburg</td>
	    <td>20:00</td>
	    <td>20:20</td>
	  </tr>
	</table>
	

To tell the data cells which header to connect to, we can use either the "scope" attribute on the header or an ID on the header and a "headers" attribute on the cells. The benefit of a scope is that it is a lot easier, the benefit of headers/ID pairs is that you can create more complex data tables. Scope can be either col for the column or row for the row the TH is in. The headers attribute describes the IDs the cell is connected as a space-separated list. An example using scope would be:

	<table>
	  <tr>
	    <th scope="col">Flight Number:</th>  
	    <th scope="col">From:</th>  
	    <th scope="col">To:</th>  
	    <th scope="col">Departure:</th>  
	    <th scope="col">Arrival:</th>  
	  </tr>
	  <tr>
	    <th scope="row">BA 3451</th>
	    <td>Heathrow</td>
	    <td>Nuremberg</td>
	    <td>19:20</td>
	    <td>19:50</td>
	  </tr>
	  <tr>
	    <th scope="row">BA 1254</th>
	    <td>Luton</td>
	    <td>Alicante</td>
	    <td>19:40</td>
	    <td>20:50</td>
	  </tr>
	  <tr>
	    <th scope="row">LH 331</th>
	    <td>Heathrow</td>
	    <td>Hamburg</td>
	    <td>20:00</td>
	    <td>20:20</td>
	  </tr>
	</table>
	

And the same table using headers/id:

	<table>
	  <tr>
	     <th id="fn" scope="col">Flight Number:</th>  
	    <th id="fr">From:</th>  
	    <th id="to">To:</th>  
	    <th id="de">Departure:</th>  
	    <th id="ar">Arrival:</th>  
	  </tr>
	  <tr>
	    <th id="f1">BA 3451</th>
	    <td headers="f1 fr">Heathrow</td>
	    <td headers="f1 to">Nuremberg</td>
	    <td headers="f1 de">19:20</td>
	    <td headers="f1 ar">19:50</td>
	  </tr>
	  <tr>
	    <th id="f2">BA 1254</th>
	    <td headers="f2 fr">Luton</td>
	    <td headers="f2 to">Alicante</td>
	    <td headers="f2 de">19:40</td>
	    <td headers="f2 ar">20:50</td>
	  </tr>
	  <tr>
	    <th id="f3">LH 331</th>
	    <td headers="f3 fr">Heathrow</td>
	    <td headers="f3 to">Hamburg</td>
	    <td headers="f3 de">20:00</td>
	    <td headers="f3 ar">20:20</td>
	  </tr>
	</table>
	

A fully accessible table also needs a summary. Furthermore it is helpful to add a caption and structure the table into a THEAD and TBODY and TFOOT:

	<table summary="This table lists all the flights by XYZ Air leaving London today.">
	  <caption>Flight Schedule</caption>
	  <thead>
	    <tr>
	       <th scope="col">Flight Number:</th>  
	      <th scope="col">From:</th>  
	      <th scope="col">To:</th>  
	      <th scope="col">Departure:</th>  
	      <th scope="col">Arrival:</th>  
	    </tr>
	  </thead>
	  <tfoot>
	    <tr>
	      <td colspan="5">Total: 3 flights</td>
	    </tr>
	  </tfoot>
	  <tbody>
	  <tr>
	    <th scope="row">BA 3451</th>
	    <td>Heathrow</td>
	    <td>Nuremberg</td>
	    <td>19:20</td>
	    <td>19:50</td>
	  </tr>
	  <tr>
	    <th scope="row">BA 1254</th>
	    <td>Luton</td>
	    <td>Alicante</td>
	    <td>19:40</td>
	    <td>20:50</td>
	  </tr>
	  <tr>
	    <th scope="row">LH 331</th>
	    <td>Heathrow</td>
	    <td>Hamburg</td>
	    <td>20:00</td>
	    <td>20:20</td>
	  </tr>
	  </tbody>
	</table>
	

The browser does not render all of these attributes, though:

Flight Schedule
Flight Number: From: To: Departure: Arrival:
Total: 3 flights
BA 3451 Heathrow Nuremberg 19:20 19:50
BA 1254 Luton Alicante 19:40 20:50
LH 331 Heathrow Hamburg 20:00 20:20

Notice that TFOOT must appear immediately after THEAD and before the first TBODY.

Now that we have the HTML skeleton, let's paint it and turn it into something like this:

Example of a styled table

Styling tables

The days of the spacer GIF

In the old days of web design, a table was styled with visual elements and attributes - markup that defined how something looks rather than what it is. As CSS was not yet defined, and browsers rendered attributes differently, we were forced to create lines, margins and paddings with "spacer" images - transparent GIFs that allowed us to set a width and height to their parent elements and allow the background colour to shine through.

This resulted in huge tables, both concerning line count and file size, and changes had to be repeated in each document. Our example table using spacer GIFs turns out to be almost 10KB and 215 lines of HTML markup.

Out with the spacer GIF - in with the (now deprecated) attributes

When the browsers of the third generation mercifully went the way of the Dodo bird, we were able to use HTML properties more cleverly to avoid all the overhead of spacer images. Padding and spacing together with a clever use of background colour allowed us to cut down our table to become roughly 2KB and 35 lines of markup. Yet we still had to replicate the colour information and padding for each table and each table element.

CSS to the rescue

With the 5th (and partly 4th) generation of MSIE coming out, we were able to use CSS properly for the first time. CSS allows us to keep the visual presentation information in a single file that gets applied to all the HTML documents. Changes can be applied a lot easier that way and we can keep the HTML free of presentational elements and attributes.

Sadly enough, however, a lot of developers didn't grasp that these newer browsers also support the aforementioned "new" table elements and attributes, and simulated these by applying classes to each element. These tables suffering from "classitis" failed miserably in conveying the flexibility of CSS. Classes became the new font tags. This is still the case for a lot of WYSIWYG tools these days. Our example table suffering from classitis amounts to about 700 bytes of CSS and 1.3K of HTML data in 44 lines.

Embracing CSS and separating "what is what" and "what it looks like"

With MSIE6 and Gecko being released, we are able to completely separate the HTML markup and the presentation.

Instead of repeating the visual information or use a lot of classes in each HTML document, we create a roughly 700 bytes big CSS file and apply it to each of the documents. Instead of classes, we use the element names and use descendant selectors. Changes are done in the CSS exclusively. Removing the classes cuts down our table to 1.3KB and 30 lines of markup .

Creating the desired "hairline" effect of our table requires some CSS skill. A quite robust way of doing it is to define a border on the left and the top for the table, and a border on the right and the bottom for the inner elements like cells and headers in our CSS.

However, to ensure that our lines meet, we need to set the cellspacing attribute in our HTML to 0, and cellspacing is a deprecated attribute for XHTML.

For XHTML documents we can use the border-collapse selector in the CSS to achieve the effect.

Lets take a look, shall we?

Our markup:

	<table summary="This table lists all the flights by XYZ Air leaving London today.">
	  <caption>Flight Schedule</caption>
	  <thead>
	    <tr>
	       <th id="fn" scope="col">Flight Number:</th>  
	      <th id="fr" scope="col">From:</th>  
	      <th id="to" scope="col">To:</th>  
	      <th id="dp" scope="col">Departure:</th>  
	      <th id="ar" scope="col">Arrival:</th>  
	    </tr>
	  </thead>
	  <tfoot>
	    <tr>
	      <td colspan="5">Total: 3 flights</td>
	    </tr>
	  </tfoot>
	  <tbody>
	  <tr>
	    <th scope="row">BA 3451</th>
	    <td>Heathrow</td>
	    <td>Nuremberg</td>
	    <td>19:20</td>
	    <td>19:50</td>
	  </tr>
	  <tr class="odd">
	    <th scope="row">BA 1254</th>
	    <td>Luton</td>
	    <td>Alicante</td>
	    <td>19:40</td>
	    <td>20:50</td>
	  </tr>
	  <tr>
	    <th scope="row">LH 331</th>
	    <td>Heathrow</td>
	    <td>Hamburg</td>
	    <td>20:00</td>
	    <td>20:20</td>
	  </tr>
	  </tbody>
	</table>
	

Our CSS

We start by defining a black border around the table and making sure that there is no spacing in between the cells. We also define the font size and family (this could be inherited from the body element, too).

	table {
		border:1px solid #000;
		border-collapse:collapse;
		font-family:arial,sans-serif;
		font-size:80%;
	}
	

We repeat the border information for all of the inner elements and set their padding to 5 pixels.

	td,th{
		border:1px solid #000;
		border-collapse:collapse;
		padding:5px;
	}	
	

We use the IDs of the headers to define the width of the columns (This could also be achieved via a colgroup and col definition in the HTML, but this is more flexible).

	#fn,#dp,#ar{width:58px;}
	#fr,#to{width:138px;}
	

Next on our list is the caption, it needs a background, a slightly larger font and a border. As our caption sits on top of the table, we have to get rid of the bottom border. Texts in captions are centered by default, therefore we need to set the text-align to left.

	caption{
		background:#ccc;
		font-size:140%;
		border:1px solid #000;
		border-bottom:none;
		padding:5px;
		text-align:left;
	}
	

The same applies to headers, and to distinguish between headers in the head and those in the body of the table, we use descendant selectors:

	thead th{
		background:#9cf;
		text-align:left;
	}
	tbody th{
		text-align:left;
		background:#69c;
	}
	

The same descendant selectors help us defining the differences for the data cells:

	tfoot td{
		text-align:right;
		font-weight:bold;
		background:#369;
	}
	tbody td{
		background:#999;	
	}
	tbody tr.odd td{
		background:#ccc;
	}
	

Voila, our table is styled. You might have realized that there is still need for a class if we want to have alternate row colours. In CSS3 we will have a chance to avoid that, via the n-th child selector, or alternatively, we can use a Javascript to add the alternate line classes for us.

More reading

This tutorial should only give you an insight of what is possible when you ditch oldschool solutions and embrace a cleaner structure. By using the right markup and create CSS that uses inheritance and descendant selectors we can cut down the amount of markup drastically, make it easier to maintain, and accessible to boot.

If you want to know more about the taming of HTML tables, make sure to read the following: