Posts from July 2020

Accordion Rows in CSS Grid

Published 4 years, 5 months past

Another aspect of the meyerweb redesign I’d like to explore is the way I’m using CSS Grid rows to give myself more layout flexibility.

First, let’s visualize the default layout of a page here on meyerweb.  It looks something like this:

A page layout diagram showing a header stretching across the top of the page, a footer stretching across the bottom of the page, and three columns of content between them: two sidebars to either side, and a main content column in the middle.

So simple, even flexbox could do it!  But that’s only if things always stay this simple.  I knew they probably wouldn’t, because the contents in those two sidebars were likely to vary from one part of the site to another — and I would want, in some cases, for the sidebar pieces to line up vertically.  Here’s an example:

A page layout diagram showing a header stretching across the top of the page, a footer stretching across the bottom of the page, and three columns of content between them. There is a main content column in the middle which stretches full-height between the header and footer. In the right sidebar, there are three boxes, labeled 'navigation', 'feeds', and 'categories', arranged in that order, top to bottom. In the left sidebar, there is a single box labeled 'archives' that isn’t as tall as the main content column. Its top edge is vertically aligned with the top edge of 'feeds', the second box in the right-hand sidebar.

That’s the basic layout of archive pages.  See how the left sidebar’s Archives lines up with the top of the Feeds box in the right sidebar?  That’s Grid for you.  I thought about lumping the Feeds and Categories into a the same grid cell (thus making them part of the same grid row), which would have meant wrapping them in a <div>, but decided keeping them separate allows more flexibility in terms of responsive rearrangement of content.  I can, for example, assign the Feeds to be followed by Archives and then Categories at mobile sizes.  Or to reverse that order.

More to the point, I also wanted the ability to place things along the bottoms of the sidebars, down near the footer but still next to the main content column, like so:

A page layout diagram similar to the previous diagram, with header and footer, main content column, and the 'archives' box in the left sidebar; and 'navigation, 'feeds', and 'categories' in the right sidebar. This figure adds 'box1' and 'box2' to the bottom of the left sidebar, and 'box3' to the bottom of the right sidebar. The bottom edges of 'box2' and 'box3' are vertically aligned with the bottom of the main column.

An early design prototype for the blog archives put the “Next post” and “Previous post” links in some of those spots, before I moved the links into the bottom of the main content column.  So at the moment, I don’t have anything making use of those spots, although the capability is there.  I could cluster content along the tops and the bottoms of the sidebars, as needed.

But here’s the important thing, and really the point of this article: I’m not rewriting the row structure and grid cell assignments for each page type.  There’s a unified row template applied to the body on every page that uses the Hamonshū design.  It is:

grid-template-rows: repeat(7,min-content) 1fr repeat(3,min-content);

The general idea here is, the first seven rows are sized to be the minimum necessary to contain content inside those rows.  This is also true of the last three rows.  And in between those sets, a 1fr row that takes up the rest of the grid container’s height, pushing the two sets apart.

In the simplest case, where there’s just a header, main content column, and a footer, with nothing in the sidebars (the layout has three columns, remember), the content will fill the rows like so:

A page layout diagram showing a header stretching across the top of the page, a footer stretching across the bottom of the page, and a main content column. There is open space to the sides of the main content column.

Here’s the Grid CSS to make that happen:

header {grid-row: 1; grid-column: 1 / -1;}
footer {grid-row: -2; grid-column: 1 / -1;}
main {grid-row: 2; grid-column: 2;}

Thus: The header fills all of row one.  The content expands row two from its placement in the center column.  The footer fills all of the last row, which is specified via grid-row: -2 (because grid-row: -1 would align its top with the bottom edge of the grid container).  There’s no more content, so all the other min-content rows have no content, so their height is zero.  And there’s no leftover height to soak up, so the 1fr row also has a height of zero.  Seems like a lot of rows specified to no real purpose, doesn’t it?

But now, let’s add some sidebar content to columns one and three; that is, the sidebars.  For example, you might remember this layout from before:

The same as figure 2. To recap: A page layout diagram showing a header stretching across the top of the page, a footer stretching across the bottom of the page, and three columns of content between them. There is a main content column in the middle which stretches full-height between the header and footer. In the right sidebar, there are

Given this setup, we can’t just assign the main content column to grid-row: 2 and leave it at that — it’s going to have to span rows.  Really, it needs to span all but the last, thus ensuring it reaches down to the footer.  So the CSS ends up like this:

header, footer {grid-row: 1; grid-column: 1/-1;}
footer {grid-row: -2;}
main {grid-column: 2; grid-row: 2/-2;}
nav {grid-row: 2; grid-column: 3;}
.archives {grid-row: 3 / span 3;}
.feeds {grid-row: 3; grid-column: 3;}
.categories {grid-row: 4; grid-column: 3;}

And as a result, the rows end up like this:

The same as the previous diagram, but in this case the main content column is taller. Purple dashed lines show where the grid lines are placed in this layout; in particular, they make it clear that the 'navigation', 'feeds', 'categories' boxes in the right-hand sidebar are placed in separate grid rows, and the 'archives' box in the left-hand sidebar spans three grid rows.
Grid-line visualization courtesy the Firefox Web Inspector.

The first set of min-content rows are all gathered up against the bottom of the top part of the layout, and the second set are all pushed down at the bottom.  Between them, the 1fr row eats up all the leftover space, which is what pushes the two sets of min-content rows apart.

I like this pattern.  It feels good to me, having two sets of rows where the individual rows accordion open to accept content when needed, and collapse to zero height when not, with a “blank” row in between the sets that pushes them apart.  It’s flexible, and even allows me to add more rows to the sets without having to rewrite all my layout styles.

As an example, suppose I decided I needed to add a few more rows to the bottom set, for use in a few specialty templates.  Because of the way things are set up, all I have to do is change the row template like this:

grid-template-rows: repeat(7,min-content) 1fr repeat(5,min-content);

That’s everything. I just changed the number of repeats in the second set of rows.  All the existing pages will continue on just fine, no layout changes, no CSS changes.  In the few (currently hypothetical) pages where I need to put a bunch of stuff along the bottom of the main content column, I just plug them in using grid-row values, whether positive or negative.  It all just works.

The same is true if more rows are added to the first set, for whatever reason.  Everything gets managed in a single CSS rule, where you can add rows for the whole site instead of having to write, track, and maintain a bunch of variants for various page types.  (Subtracting rows is harder without causing layout upset, but could still be done in some scenarios.)

As a final note, you’re probably wondering: Is that one 1fr row actually necessary to get a layout like this one?  Not really, no.  Let’s take it out, like this:

grid-template-rows: repeat(11,min-content);

What happens as a result is the rows that aren’t directly occupied by content (the ones that previously collapsed to zero height), but are still spanned by content (the center column), divvy up the leftover space the 1fr row used to consume.  This leads to a situation like so:

The same layout diagram as the previous figure, but with some of the grid rows placed differently.

To the user, there’s no practical difference.  Things go to the same places either way.  You just get the “extra” rows stretching out, instead of being pushed apart by the 1fr row.

I certainly could have left it at that, and it arguably would have been cleaner to do so.  But something about this approach doesn’t sit quite right with me; there’s a tickly feeling in the back of my instinct that tells me there’s a downside to this.  Admittedly, it could be a vestigial instinct from the Age of Floats; I doubtless have many things I still have not unlearned.  On the other hand, it could be something about Grid I’ve picked up on subconsciously but haven’t yet brought into full realization.

If I ever pin the tickle down enough to articulate it, I’ll update the post to include it.

Thanks to Kitt Hodsden and Laura Kalbag for their assistance with this article.


Browse the Archive