Data tables
Tabular data must be presented with a sound semantic and visual structure
- Version:
- 0.1.0
- Status:
- Published
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.
Recommended markup
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.
Recommended layout
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-gradient
s 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.
Recommended behaviour
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
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 |
Related research
This topic does not yet have any related research available.
Further reading, elsewhere on the Web
Layout Tables Versus Data Tables — WebAim, https://webaim.org/techniques/tables/#uses ↩︎
VoiceOver and Tables with an Empty First Header Cell, http://accessibleculture.org/articles/2010/10/voiceover-and-tables-with-an-empty-first-header-cell/ ↩︎
The Table Caption element — MDN, https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption ↩︎
Pure CSS scrolling shadows with
background-attachment: local
, http://lea.verou.me/2012/04/background-attachment-local/ ↩︎