X-Git-Url: https://git.draconx.ca/gitweb/homepage.git/blobdiff_plain/f2f70556922d006af212d3cd977450e8d140ee84..ec1703ff7346b6d3edeee73f1c6dfa750b7aeed4:/content/weblog/responsive-tables.md diff --git a/content/weblog/responsive-tables.md b/content/weblog/responsive-tables.md new file mode 100644 index 0000000..2063484 --- /dev/null +++ b/content/weblog/responsive-tables.md @@ -0,0 +1,442 @@ +--- +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.