Skip to content

Data tables

Tabular data must be presented with a sound semantic and visual structure

Version:
0.1.0
Status:
Draft

Introduction

BBC tables follow longstanding conventions for the structuring of tabular data. However, some enhancements are included in the following implementation.

Since wrapping table cells, for the purposes of responsive design, would make a nonsense of the data structure, data tables must retain their rigid, two-dimensional arrangement. Accommodating tables with many columns therefore becomes a question of allowing horizontal scrolling. Since horizontal scrolling is typically avoided, and may be unexpected, it must only be applied where necessary, with both additional visual affordance (see Recommended layout) and keyboard interaction supported.

The following implementation also includes 'sticky' column header support, to help users peruse tables with a considerable number of rows, and the option to apply column sorting functionality.

No matter the templating or rendering technology, data tables should be marked up with the <table> and associated elements. Data tables composed from <div> elements require considerable ARIA attribution to elicit the expected screen reader behaviors, and non-trivial amounts of CSS to emulate the grid-like layout behavior.

Column headers

Any <table> that does not have column or row headers (<th> elements) cannot be considered a true data table. In fact, some screen readers that encounter a table without headers will treat it as a 'layout table' and communicate it quite differently[1].

Bad example

<table>
  <tr>
    <td>Fake column header 1</td>
    <td>Fake column header 2</td>
    <td>Fake column header 3</td>
  </tr>
  <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 3, cell 2</td>
    <td>Row 4, cell 3</td>
  </tr>
</table>

Good example

<table>
  <tr>
    <th>Column header 1</th>
    <th>Column header 2</th>
    <th>Column header 3</th>
  </tr>
  <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 3, cell 2</td>
    <td>Row 4, cell 3</td>
  </tr>
</table>

Table header elements are labels for column data. With the column headers in place, when you navigate from a table cell in one column to a table cell in an adjacent column, the new column's <th> content is announced for context. This enables users to traverse and understand tables non-visually.

Row headers

In most cases, column headers suffice. However, in some tables, the first cell of each row can be considered the 'key' cell and acts like a header for the row. It's recommended you differentiate between column and row headers explicitly using the scope attribute:

<table>
  <thead>
    <tr>
      <th scope="col">Column header 1</th>
      <th scope="col">Column header 2</th>
      <th scope="col">Column header 3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Row 1 header</th>
      <td>Row 1, cell 2</td>
      <td>Row 1, cell 3</td>
    </tr>
    <tr>
      <th scope="row">Row 2 header</th>
      <td>Row 3, cell 2</td>
      <td>Row 4, cell 3</td>
    </tr>
  </tbody>
</table>

Note that the <table> is now divided into a head (<thead>) and body (<tbody>). This does not have any impact on the behavior of the table and its headers. But, as we enhance the table, these elements will come in useful for styling and scripting.

Captions

So far, we have labelled the rows and columns, but not the table itself. You may be inclined to introduce a table with a heading element, such as an <h2>. This would certainly aid users browsing the page visually. However, a heading would not be directly associated with the table, meaning a screen reader user navigating directly to the table (using a shortcut like T in NVDA) would not hear a label announced upon arrival.

Instead, provide a <caption>[3]. Where there is already a requirement for a heading and you want to eliminate repetition and redundancy, it is permitted to place the heading inside the <caption> element.

<table>
  <caption>
    <h2>Example table</h2>
  </caption>
  <!-- table headers and data -->
</table>

This will mean screen reader users can reach the table via either table or heading shortcuts provided by their software. In either case, it will be identified by the caption/heading's text.

Importantly, the grid structure of data tables must remain intact no matter the available space. That is, elements must not wrap or otherwise change position since they will become labelled incorrectly. For tables with many columns this may result in horizontal scrolling. It is recommended a container element with overflow-x: auto is used to contain the horizontal scroll behavior.

.gel-table {
  overflow-x: auto;
}

To make this element scrollable by keyboard, it must first be focusable. This requires the tabindex="0" attribution. For screen reader users, this newly interactive element will need a label. It's recommended the element takes the group role and is associated with the <caption> for the labelling.

<div class="gel-table" role="group" aria-labelledby="caption" tabindex="0">
  <table>
    <caption id="caption">
      <h2>Example table</h2>
    </caption>
    <!-- table headers and data -->
  </table>
</div>

Indicating scroll functionality visually

Currently, only the bisection of a column indicates an an overflow, and the ability to scroll more data into view. This does not provide a great deal of affordance. In addition, you can apply an indicative shadow/fade to whichever side the overflow is occurring at.

A set of linear-gradients with differing background-attachment values are employed to achieve this effect:

.gel-table {
  overflow-x: auto;
  background-color: #fff;
  background-image: 
    linear-gradient(90deg, #fff 0%, transparent 4rem),
    linear-gradient(90deg, rgba(0, 0, 0, 0.3) 0%, transparent 2rem),
    linear-gradient(270deg, #fff 0%, transparent 4rem),
    linear-gradient(270deg, rgba(0, 0, 0, 0.3) 0%, transparent 2rem);
  background-attachment: local, scroll;
}

Where the is no overflow, the white local gradient masks the grey scroll one and hides it[4].

Sticky headers

Where there are numerous rows, it's possible to scroll past the headers, making interpreting the data more difficult (since it would require either a good memory, or a lot of scrolling up and down). The conventional solution to this issue is to make the column header row 'sticky', so it follows the user down the page as a persistent reference.

This is now possible in CSS, with the position: sticky declaration. However, containers with an explicit overflow such as that applied in the last section, will forego the position: sticky behavior. The following is therefore provided as an enhancement for tables not producing an overflow.

.gel-table th {
  position: sticky;
  top: 0;
}

The overflow-x: auto style is removed dynamically, via ResizeObserver in the Reference implementation, where no overflow is occurring. This reinstates the sticky header behavior.

The handling of overflow/scrolling behaviour already covered in the Recommended layout is handled progressively, by first feature detecting ResizeObserver.

if ('ResizeObserver' in window) {
  var ro = new ResizeObserver(entries => {
    for (var entry of entries) {
      var cr = entry.contentRect;
      var noScroll = cr.width >= this.table.offsetWidth;
      entry.target.tabIndex = noScroll ? -1 : 0;
      entry.target.style.overflowX = noScroll ? 'visible' : 'auto';
      this.thead.classList.toggle('sticky', noScroll);
    }
  });

  ro.observe(elem);
}

Where ResizeObserver (or JavaScript) is not available, the table container acts as if it is liable to scroll, with overflow-x: auto set, and tabindex="0" (for keyboard control over scrolling) intact.

Sorting

In addition, sorting functionality is provided where the Reference implementation constructor's second argument is set to true.

var tableContainer = document.querySelector('.gel-table');
var table = new gel.Table.constructor(tableContainer, true);

The table is progressively enhanced to include sorting buttons for each of the column headers. These are each labelled 'sort' using a visually hidden <span class="gel-sr" />, and display the re-order icon from the GEL iconography suite.

<th scope="col" aria-sort="none">
  Teams
  <button>
    <span class="">Sort</span> 
    <svg viewBox="0 0 32 32" class="gel-icon gel-icon--text" focusable="false" aria-hidden="true">
      <path d="M18.033 25.5v-19l5.6 5.7 2.4-2.4-10-9.8-10 9.8 2.4 2.4 5.6-5.7v19l-5.6-5.7-2.4 2.4 10 9.8 10-9.8-2.4-2.4"></path>
    </svg>
  </button>
</th>

When a user clicks a header's sort button, ascending order is prioritized and aria-sort's value switches from none to ascending. Subsequent clicks to the same button will toggle the order between ascending and descending (aria-sort="descending"). All columns not being used to sort have headers with the none value.

Sorting is based on the text content of cells, meaning any HTML can be used without breaking the sorting algorithm.

Reference implementation

Premier League

Team Played Won Drawn Lost For Against Goal Difference Points
Wolves 38 16 9 13 47 46 1 57
West Ham 38 15 7 16 52 55 -3 52
Watford 38 14 8 16 52 59 -7 50
Tottenham 38 23 2 13 67 39 28 71
Southampton 38 9 12 17 45 65 -20 39
Newcastle 38 12 9 17 42 48 -6 45
Man Utd 38 19 9 10 65 54 11 66
Man City 38 32 2 4 95 23 72 98
Liverpool 38 30 7 1 89 22 67 97
Leicester 38 15 7 16 51 48 3 52
Huddersfield 38 3 7 28 22 76 -54 16
Fulham 38 7 5 26 34 81 -47 26
Everton 38 15 9 14 54 46 8 54
Crystal Palace 38 14 7 17 51 53 -2 49
Chelsea 38 21 9 8 63 39 24 72
Cardiff 38 10 4 24 34 69 -35 34
Burnley 38 11 7 20 45 68 -23 40
Brighton 38 9 9 20 35 60 -25 36
Arsenal 38 21 7 10 73 51 22 70
Bournemouth 38 13 6 19 56 70 -14 45

Open in new window

This topic does not yet have any related research available.

Further reading, elsewhere on the Web


  1. Layout Tables Versus Data Tables — WebAim, https://webaim.org/techniques/tables/#uses ↩︎

  2. VoiceOver and Tables with an Empty First Header Cell, http://accessibleculture.org/articles/2010/10/voiceover-and-tables-with-an-empty-first-header-cell/ ↩︎

  3. The Table Caption element — MDN, https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption ↩︎

  4. Pure CSS scrolling shadows with background-attachment: local, http://lea.verou.me/2012/04/background-attachment-local/ ↩︎

Copyright © 2019 BBC. This content is published under the Open Government Licence, unless otherwise noted.

Got a minute?

We'd love to know what you think of our website.

Go to survey