---
title: Adventure in Responsive WWW Tables
copyright: 2020 Nick Bowler
license: cc-by-sa-4.0
published: 2020-07-05T23:25:02-0400
---
*[CSS]: Cascading Style Sheets
*[HTML]: HyperText Markup Language
A quest to improve table display on the World Wide Web, using modern
technologies that degrade gracefully.
Sometimes, when presenting tables, better use of screen (or paper) space is
achieved by displaying table rows in two (or more) side-by-side vertical
columns. I would generally use this approach whenever rows can be reasonably
formatted to fit in less than half the page width and the table is sufficiently
long.
For an example, compare and contrast [table 1](#t1) (a straightforward HTML
table) with [table 2](#t2) (exactly the same information, split into two
sets of columns).
ID | Name | Age
---|-----------|----
1 | Barbara | 34
2 | Charles | 42
3 | David | 53
4 | Elizabeth | 32
5 | James | 33
6 | Jennifer | 38
7 | Jessica | 37
8 | John | 31
9 | Joseph | 38
10 | Karen | 57
{:#t1 caption="<%= counter(:table) %>:
An inefficient use of available space"}
ID | Name | Age | ID | Name | Age
---|-----------|-----|----|----------|----
1 | Barbara | 34 | 6 | Jennifer | 38
2 | Charles | 42 | 7 | Jessica | 37
3 | David | 53 | 8 | John | 31
4 | Elizabeth | 32 | 9 | Joseph | 38
5 | James | 33 | 10 | Karen | 57
{:#t2 caption="<%= counter(:table) %>:
Same information, more compact presentation"}
This works, but there is a significant problem: the presentation of [table
2](#t2) into two sets of columns is hardcoded in the HTML markup. Aside from a
general aversion to mixing content with style, there is a practical problem: on
a narrow display the table could easily be too wide. If columns end up off the
screen this would be worse than [table 1](#t1) which probably fits nicely.
What we really want is for the user agent to automatically select the most
appropriate presentation for display---a so-called responsive layout.
# Enter CSS Grid
[cssgrid]: //www.w3.org/TR/css-grid-1/
The [CSS grid module][cssgrid] enables CSS rules to define a table-like
presentation, with quite a lot of flexibility. Unfortunately CSS grids
have a number of major practical problems and limitations. We will discuss
and overcome some of these limitations to achieve a responsive layout for
our table with a high degree of compatibility.
The first and most obvious problem is that not all browsers support CSS grids.
Whatever solution we implement must, to the greatest extent possible, work in
all browsers. This means graceful degradation: if the grid does not work for
any reason we want to get a readable table, ideally one like [table 1](#t1).
Even with the latest and greatest browser, there is no guarantee the styles
actually work because they might not even load. Firefox still lets users
disable styles at the touch of a button. Our table must be readable in
these scenarios.
With a CSS grid, the _grid items_ are all the child elements of the _grid
container_, which is an element styled with `display: grid`. In particular,
descendent elements that are not direct children of the grid container do not
participate in the grid layout. While the CSS `display: contents` style exists
to help overcome this limitation, it turns out to not be very useful: among
browsers that support grids, not all support `display: contents`. Thus we
cannot expect grid layouts to work if we depend on `display: contents`.
If you search online for laying out tabular data with CSS grid, you will find
many examples that flatten all the table cells into a linear sequence of
elements within a big container `div` or similar. This satisfies the markup
constraints but is, quite frankly, terrible. The structure of the data has
been moved from the markup into the stylesheet. The table is meaningless if
the stylesheet is not working for any reason. Whatever happened to separation
of content and style?
To achieve graceful degradation, we must start with markup that looks very
similar to the markup of [table 1](#t1)---a straightforward HTML table, and
only apply styles on top of that.
## Rows as grid items
The markup constraints introduce an immediate practical problem: the actual
table cells cannot be the primary grid items for our responsive layout,
because we need to arrange items row-by-row. So instead, our table rows will
be the grid items. Therefore the grid containers must be `thead` and `tbody`.
We will not bother with `tfoot` at this time, but it should be pretty similar
to `thead`.
Using a media query, a simple 2-column grid can be configured which fills the
available space only if there is sufficient width available.
#### <%= counter(:listing) %>: Markup for [table 3](#t3)
#### <%= counter(:listing) %>: Style for [table 3](#t3)
<%= counter(:table) %>: First layout attempt with grid
Header |
Row 1 |
Row 2 |
Row 3 |
Row 4 |
Row 5 |
Row 6 |
[Table 3](#t3) shows the basic structure working but there are several
obvious deficiencies. The most glaring is the lack of a header on the
right-hand grid column. Some of the table styling (odd/even row shading
in particular) is busted. And the rows are not in the desired location:
row 2 should be beneath row 1, not beside it.
## Duplicating the header
There is unfortunately no way to duplicate the header in a stylesheet, so we
have no choice but to duplicate the header row in the table markup itself. We
can hide it with an inline style attribute and reveal it when the grid is
enabled in the media query. An inline `display: none` is widely supported to
hide this row normally.
#### <%= counter(:listing) %>: Markup for [table 4](#t4)
#### <%= counter(:listing) %>: Style for [table 4](#t4)
<%= counter(:table) %>: Duplicated header on grid
Header |
Header (duplicated) |
Row 1 |
Row 2 |
Row 3 |
Row 4 |
Row 5 |
Row 6 |
This is not perfect: the duplicate header will appear if styles are disabled
or if the browser does not support CSS at all, which includes all text-mode
browsers that I am aware of. But it is a fairly minor annoyance and the
situation can be improved somewhat with some hacks that we might explore in
another adventure.
## Fixing the row placement
Our grid currently consists of two grid columns and a dynamic number of
grid rows, and the automatic grid layout fills each row before creating
a new one. We can place the rows where they need to go by styling the
first half of the rows differently from the last half. The first half
can be forced to the first column, and the remainder will auto-place
to the correct locations when using `grid-auto-flow: dense`.
Perhaps the easiest way to do this is to markup the halfway point with a
class, so that CSS selectors can reference this row without hardcoding the
number of rows in the table.
#### <%= counter(:listing) %>: Markup for [table 5](#t5)
#### <%= counter(:listing) %>: Style for [table 5](#t5)
<%= counter(:table) %>: Correct row placement on grid
Header |
Header (duplicated) |
Row 1 |
Row 2 |
Row 3 |
Row 4 |
Row 5 |
Row 6 |
## Fixing the row styling
Depending on the style used, [table 5](#t5) might be good enough. But here,
the odd/even row shading is messed up because the split was not performed
on an even-numbered row, and there is no bottom border on the first grid
column. Now that the the row positioning is correct, both of these issues
are pretty easy to fix in the stylesheet.
#### <%= counter(:listing) %>: Markup for [table 6](#t6)
#### <%= counter(:listing) %>: Style for [table 6](#t6)
<%= counter(:table) %>: Correct row styling on grid
Header |
Header (duplicated) |
Row 1 |
Row 2 |
Row 3 |
Row 4 |
Row 5 |
Row 6 |
# Realistic Tables
[Table 6](#t6) is pretty close to what we want, except for the minor detail
that real tables typically have more than one column. So let's try applying
these techniques to [table 1](#t1).
#### <%= counter(:listing) %>: Markup for [table 7](#t7)
#### <%= counter(:listing) %>: Style for [table 7](#t7)
<%= counter(:table) %>: Column misalignment on grid
ID | Name | Age |
ID | Name | Age |
1 | Barbara | 34 |
2 | Charles | 42 |
3 | David | 53 |
4 | Elizabeth | 32 |
5 | James | 33 |
6 | Jennifer | 38 |
7 | Jessica | 37 |
8 | John | 31 |
9 | Joseph | 38 |
10 | Karen | 57 |
The fallback for [table 7](#t7) works properly but when the grid columns are
activated, the alignment of table cells into their respective columns is not
correct. This is something that we could potentially solve with CSS subgrids,
but as with `display: contents` lack of browser support makes their use
untenable.
Nevertheless, provided that the width of each cell is independent of its
contents, they will all line up correctly. There are many ways to do this
in a stylesheet. We will do it with more grids.
#### <%= counter(:listing) %>: Markup for [table 8](#t8)
#### <%= counter(:listing) %>: Style for [table 8](#t8)
<%= counter(:table) %>: Fully working responsive table
ID | Name | Age |
ID | Name | Age |
1 | Barbara | 34 |
2 | Charles | 42 |
3 | David | 53 |
4 | Elizabeth | 32 |
5 | James | 33 |
6 | Jennifer | 38 |
7 | Jessica | 37 |
8 | John | 31 |
9 | Joseph | 38 |
10 | Karen | 57 |
# Epilogue
Some issues remain, but I think the results in [table 8](#t8) are pretty good.
I don't consider the loss of automatic cell sizing to be a big deal, sizes
usually need to be tweaked per-table anyway and the use of `fr` units can
give pretty nice results that scale with available width.
More than two grid columns should be possible but I have not attempted to do
so. There might be a lot more fighting with the automatic grid placement.
Depending on the table it may be useful to tweak various alignment properties
of the grid items.
It seems that Firefox really sucks at printing these grids if they happen to
cross a page boundary (I did not try printing in other browsers). Firefox is
pretty bad at printing regular tables when they do this too, but it is way
worse with grids. Using e.g. `@media screen` in the stylesheet can help by
falling back to an ordinary table for printouts.