Grid-ish Flexbox or Flexible Grid?

Whilst building the new reviews section on this site I had a few moments when I needed a layout somewhere between CSS grid and flexbox. I tend to find that this kind of pattern crops up in one of two situations:

  1. Either I need a row of elements that is able to wrap based on available container width;
  2. Or I want to always display elements as a grid but don't want empty cells, regardless of how many elements are present.

At a glance, scenario one should use flexbox whilst scenario two feels like an obvious fit for a grid layout and, ultimately, that's where I settled. Except both had little gotchas which I thought would be worth writing up, so I can remember how I solved them next time. I came to think of these two options as a "grid-ish flexbox" and a "flexible grid" (and yes, the second name is a lot nicer to say 😉), so that's what I'll refer to them as.

Grid-ish Flexbox

The most obvious example is the filter tags I have on my site search. These are a list of categories or filters which I define in Algolia and then make available as buttons. The number of tags can (and will) increase/decrease over time and also dynamically show or hide themselves depending on the other search parameters; there's no point in allowing someone to filter on a tag which has zero results, after all. It looks something like this:

Video of typing into search box where a list of filter options shrinks with each keystroke.
See what I mean about it needing to be flexible in terms of element numbers.

It's created from an array of tags iterated over with a map() function, before applying some fancy CSS. Under the hood, they're actually a sequence of unordered list items (<li>) containing checkboxes, allowing the user to toggle between on/off states accessibly, but that's not really important. Just think of them as a sequence of DOM elements of unknowable length, that need to sit alongside one another whilst filling their container.

In other words, at their smallest sizes they will always form a row. Flexbox is best suited for one-dimensional layouts like this, hence why it's the obvious starting point:

ul {
    display: flex;
    list-style: none;
}

That gets us from a list to this:

Filter buttons in a row but text is cramped and overlapping and the final box is half cut off by the window edge.
I find it weird how much these now look like Dr Mario pills...

Ew 🤢 Well, that's obviously not great. The elements are trying to be as small as possible, making them unreadable, and they still don't fit within the parent container so they're just disappearing off the right edge of the screen. On larger displays it probably looks fine, which is why designing mobile-first is so beneficial, allowing us to catch this issue early. Luckily, flexbox has the ideal solution: flex-wrap: wrap;!

With that now applied, our filter options are able to break out of that initial row and become clear and readable. Nice 🙌:

Filter options like "technology" and "frontend" in a mosaic grid pattern.
Worlds better but still incredibly cramped. I can already predict the misclicks 🤦‍♂️

That's looking a lot better but it's pretty cramped. I'm not worried about creating a perfect grid, so the right side can remain a bit fragmented (I actually quite like that it isn't perfectly aligned), but I do want some negative space around each filter.

In Firefox – and hopefully all other browsers some point soon (please! 🙏) – we can do this beautifully with the gap property:

ul {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    list-style: none;
}

Which gives us:

A neatly arranged list of filter options, in multiple columns and rows with gaps between them.
Why haven't Chromium added universal gap support 😭

Perfect ⭐ We're done! Until you load it up in Chrome or Edge or anything else and... yeah, the list doesn't look any different at all. * sigh * Right, back to the drawing board. How else can we add negative space? With margin, of course. Let's add a top-margin and right-margin to each <li>:

li {
    margin: 0.4rem 0 0 0.4rem;
}

That's looking pretty solid, but now we're getting some unwanted extra space on the left-most elements and on the top row (see how it doesn't align with the elements above/below):

The same list of filter options but now with some unwanted extra space causing it to not align with other items on the page.
It's subtle but still ugly.

To fix that, we can add a negative margin to the parent <ul> and that should give us exactly the same look as flex-gap did:

ul {
    display: flex;
    flex-wrap: wrap;
    /* gap: 0.4rem; waiting for better support */
    margin: -0.4rem 0 0 -0.4rem;
    list-style: none;
}

And there you have it. It's a flexbox layout that will default to a single row for searches that contain only a few filter options (or on very wide screens), but otherwise will just neatly wrap onto as many new rows as it needs whilst still preserving row- and column-gaps. It looks a little like a grid but with much more, well, flexibility; AKA a grid-ish flexbox. Nice 👍

Now to be clear, I'm sure there are plenty of other ways to achieve this. I experimented with things like space-between and even an actual grid with auto-fill, but nothing was either as simple to understand or as flexible as the above.

Flexible Grid

Okay, but what if you want everything to be neat and ordered, aligned to both the left and right container boundaries? Well at that point, it's probably better to switch from flexbox to grid completely, as you're definitely working in two-dimensions as your default layout (i.e. both rows and columns). Personally, that's what I wanted my card "categories" to look like (here shown as "series and collections"):

Review card showing rating, link to read the review, and three categories displayed in a grid: "avengers", "mcu", and "marvel".
I'm really happy with how this came out 😀

Again, under the hood these are made from an unordered list containing a series of links, with the exact categories being dynamically generated from my CMS via a JSON API. That means I can have one, two, three, or a dozen possible links and the grid needs to be able to accommodate every specific variation. However, I do have a useful limitation, which is that I only ever want a maximum of two columns. If you had a grid which could scale the number of columns available, you might not be able to use this same technique, but I'll get on to that in a moment.

For now, let's take a look at the initial grid requirements:

ul {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem;
}

Thanks to the much better browser support for both grid-gap and flexible column units (fr) we can happily make use of both to solve 90% of the potential trickiness of this layout. Isn't grid great? Getting something this neat ten years ago would have been a nightmare (* shudders *). The tags are automatically arranged in flexible-width columns, which neatly align with both edges of the container whilst having clear gutters between them:

The three categories, only the final tag ("marvel") is followed by a large blank space.
I mean, for an initial effort that's frighteningly good, particularly if you've been working with CSS as long as I have.

However, that third category is stuck in the first column, which leaves a great big ugly blank space. Ew once again 🤢 The trick is to let that category span both columns, but you've got to be careful about how you go about doing so. The initially obvious solution (to me at least) was to target the last category in the list and let it span everything[1]:

li:last-of-type {
    grid-column: 1 / -1;
}

Except, that means that if you have an even number of categories, you'll just shift the blank space up a row:

Four categories in a grid, where the final tag spans both columns leaving a blank cell on row two.
Whoops, did not think that through!

Instead, we can use the incredibly clever combination of the :nth-child(odd) and :last-child selectors to ensure the full-width elements only ever appear as the last instance where there is an uneven number of elements:

li:last-of-type:nth-child(odd) {
    grid-column: 1 / -1;
}

Hence why having only two columns is massively helpful for this technique. It's fairly simple to adapt this part for 3+ columns by toggling between nth-child(even) and nth-child(odd) as necessary, but if you don't know how many columns you might have then I think it becomes impossible (without JavaScript, that is). Even with a fixed number of columns, you need to consider what happens if your element array falls on the middle column. For example, if you have a three-column grid and five elements, making the final element span 1 / -1 will create two empty cells above it. I believe you could use grid-column: auto / -1 but I haven't actually tested it.

At any rate, for my uses the final result is an ideally flexible grid:

Two review cards side-by-side, one with four categories in a two-column grid, the other with three where the final spans both columns.
It works just as well with one category or a dozen categories. Satisfying 😎

Perfection 👍

Explore Other Articles

Newer

Be Curious About Your Code

I've been thinking a lot about an article I read recently that called out technical writing online for being overly trusted. But shouldn't that same argument apply more universally to third-party code coming from any source?

Older

JSNation Live 2020 Notes

Another month, another big and fully remote JavaScript conference. JSNation fit into my schedule a little less (and didn't quite overlap with my interests as neatly) but it was a fun event with some interesting talks on topics that are often only on my periphery. Much to think about!

Conversation

Want to take part?

Comments are powered by Webmentions; if you know what that means, do your thing 👍

Footnotes

  • <p>The new dominant layout methods in CSS – grid and flexbox – have solved a lot of issues. Still, sometimes the ideal layout is somewhere in the middle: a flexible grid-like mashup. With a bit of outside-the-box thinking, you can there from either angle.</p>
  • Murray Adcock.
Article permalink

Made By Me, But Made Possible By:

CMS:

Build: Gatsby

Deployment: GitHub

Hosting: Netlify

Connect With Me:

Twitter Twitter

Instagram Instragram

500px 500px

GitHub GitHub

Keep Up To Date:

All Posts RSS feed.

Articles RSS feed.

Journal RSS feed.

Notes RSS feed.