voice-family: 'Gary Lineker';

26th November 2005

Recently at work I was presented with a football news item to add to our website — Alton College had beaten St Vincent College 3 - 1 to move level on points at the top of the Hampshire College’s under-19 League. As part of the news item there was a league table highlighting Alton’s current lofty position. Whilst marking up the table I thought it might be interesting/fun to experiment with adding a little bit of speech CSS and generated content to make the table easier to follow when read using Opera.

When a table is read linearly it can be very easy to get lost and become confused as to what the data in the current cell represents. HTML provides several tags and attributes that can be used to create better structured tables that are easier for screen reader users to follow. For example use of the th tag and headers attribute in a table cell provides a connection between that cell and it’s row/column header(s). Roger Hudson’s Accessible Data Tables has some good examples of this.

Opera 8+ can also be used as a simple screen reader, and while it doesn’t provide it’s own tools to aid reading tables it does support the CSS 3 Speech Module, which combined with CSS generated content can make a table more easy to understand by providing additional spoken hints.

The Markup

The abbreviated and slightly modified (e.g. acronym replaced by abbr for purposes of code snobbery) markup follows,

<table summary="The standings in the Hampshire Colleges’ 
Under 19 Football League as of 9th November 2005." 
<caption>Hampshire Colleges’ Under 19 League</caption>
    <th scope="col" id="played"><abbr title="Matches 
    <th scope="col" id="won"><abbr title="Matches 
    <th scope="col" id="drawn"><abbr title="Matches 
    <th scope="col" id="lost"><abbr title="Matches 
    <th scope="col" id="goals-for"><abbr title="Goals 
    <th scope="col" id="goals-against"><abbr title="Goals 
    <th scope="col" id="points"><abbr  
  <tr class="alt-row">
    <th scope="row" id="team-bcot"><abbr title="Basingstoke 
College of Technology">BCOT</abbr></th>
    <td headers="played team-bcot">5</td>
    <td headers="won team-bcot">5</td>
    <td headers="drawn team-bcot">0</td>
    <td headers="lost team-bcot">0</td>
    <td headers="goals-for team-bcot">22</td>
    <td headers="goals-against team-bcot">4</td>
    <td headers="points team-bcot">15</td>
  <tr class="highlighted-row">
    <th scope="row" id="team-alton">Alton</th>
    <td headers="played team-alton">6</td>
    <td headers="won team-alton">5</td>
    <td headers="drawn team-alton">0</td>
    <td headers="lost team-alton">1</td>
    <td headers="goals-for team-alton">22</td>
    <td headers="goals-against team-alton">9</td>
    <td headers="points team-alton">15</td>
  <tr class="alt-row">
    <th scope="row" id="team-tauntons">Tauntons</th>
    <td headers="played team-tauntons">4</td>
    <td headers="won team-tauntons">4</td>
    <td headers="drawn team-tauntons">0</td>
    <td headers="lost team-tauntons">0</td>
    <td headers="goals-for team-tauntons">25</td>
    <td headers="goals-against team-tauntons">3</td>
    <td headers="points team-tauntons">12</td>
    <th scope="row" id="team-itchen">Itchen</th>
    <td headers="played team-itchen">5</td>
    <td headers="won team-itchen">4</td>
    <td headers="drawn team-itchen">0</td>
    <td headers="lost team-itchen">1</td>
    <td headers="goals-for team-itchen">22</td>
    <td headers="goals-against team-itchen">9</td>
    <td headers="points team-itchen">12</td>

If this is read out in Opera (League table before speech CSS — select the table and press V to read) without any additional speech CSS you should hear something like the following:

Hampshire Colleges’ Under Nineteen League. P, W, D, L, F, A, Pts, BCOT, five, five, zero, zero, twenty-two, four, fifteen, Alton, six, five, zero, one, twenty-two, nine, fifteen, Tauntons…

Which isn’t really all that clear. To start improving this we could have the browser read the results in the format,

Team name, Played {matches played} Won {matches won} Drawn {matches drawn} Lost {matches lost} Goals for {goals scored} Goals against {goals conceded}, {number of points} Points.

This is reasonably simple, the headers attribute can be used to create a selector that matches the different cells, then the appropriate descriptive text can be inserted either before or after the element using generated content. For example, the following would insert the text “Played ” before any cell in the table that contained the word “played” in it’s headers attribute.

table.league-table td[headers~="played"]::before {
  content: 'Played ';

The table also contains a few abbreviations in the headings, such as P for Matches Played and BCOT for Basingstoke College of Technology. The table might read better if these abbreviations were expanded, so instead of “P” the browser read “Matches played”. So the title attribute of the abbr tag should be read out, but it’s content should’t be. This is done with the following CSS,

table.league-table th abbr {
  speak: none;
table.league-table th abbr::before {
  speak: normal;
  content: attr(title);

The attr function returns the value of the supplied attribute, in this case title.

However adding all this generated content makes the page look very untidy, it would be good if the generated content could be hidden, but still be read out. Unfortunately the obvious choice — display: none; — will prevent the content being read out. The generated content will be read out if visibility: hidden; is used to hide it, however because visibility: hidden; still leaves space for the hidden content the generated content also needs to be made as small as possible so as not to leave big gaps in the layout.

table.league-table *::before, table.league-table *::after {
  visibility: hidden;
  display: inline-block;
  width: 1px; height: 1px;
  overflow: hidden;

Speech CSS can also be used to add pauses in the reading. This could be used to add a short break after the team name (which is found in a th tag with a scope attribute equal to “row”) and again at the end of a row,

table.league-table tr {
  pause-after: 750ms;
table.league-table th[scope="row"] {
  pause-after: 750ms;

On reflection, perhaps reading out the column headings in the table is unnecessary. After all the browser is now telling us what the value in the cell refers to as it reads it. The following would prevent Opera from reading the abbreviations in the first row of the table:

table.league-table tr:first-child abbr::before {
  speak: none;

Now when read in Opera (try the League table with speech CSS) things should make a little more sense. It’s quite fun, although it’s usefulness is obviously limited by the lack of browser support.

Permalink. Posted on 26th November 2005 in Browsers, CSS, Web Standards.


There are currently no comments for this item.

Sorry, comments for this item are currently closed.

Of Interest