X-Git-Url: http://git.draconx.ca/gitweb/homepage.git/blobdiff_plain/f2f70556922d006af212d3cd977450e8d140ee84..ec1703ff7346b6d3edeee73f1c6dfa750b7aeed4:/content/weblog/responsive-tables.md?ds=inline
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.