Flex items are not grid columns

kenbellows

Ken Bellows

Posted on April 1, 2019

Flex items are not grid columns

We often talk about Flexbox's growy-shrinky rules, like flex-grow and justify/align-content/items, as working basically identically to the fr unit introduced with CSS Grid. That works as an introduction to frs for devs who are familiar with Flexbox, but it's not really true. There's a subtle but very important difference that sometimes shows up.

Scenario: Three differently-sized text items in a row

Let me lay down a scenario, as I love to do. This is a specific case of a general scenario that I have encountered pretty often, and it was a common frustration that's solved neatly by CSS Grid.

Consider a case where we have 3 inline elements, e.g. text labels, buttons, or images, of different sizes that need to be displayed neatly in a row. We want the first label to be left-aligned, the last one to be right-aligned, and the middle one to be centered on the page.

Here's an example of this that comes from an old weekend project of mine from college (that I hope to write more extensively about in future articles, but that's beside the point). This project was an experiment with the HTML5 <canvas> element, and it had an element of keyboard and mouse interaction. There were three primary controls, and I had three text labels above the canvas listing these controls:

Three text labels

So that's the goal. The most important requirement, the one that will cause problems, is that the middle item must be centered on the page: its center line should match the center line of the page. (In some cases, "page" is just "container", but "page" is easier to demonstrate.) I've included the heading above the labels to give a useful visual landmark to compare against.

Old school cool

Back in college, in the pre-flexbox days of 2012(ish), I solved this with some very brittle HTML+CSS that used a lot of absolute positioning:

I've absolutely positioned the first and last labels to be on the same line as the middle element, stuck to the left or right. This... works, of course, but could be disrupted by certain changes to the surrounding layout, is very non-reactive, is pretty unreadable, and is inflexible: if you wanted to add a fourth control, everything would become a lot more complex.

Flexbox

If we were going to write this with modern CSS, the first thought might be to use Flexbox. It was certainly my first thought; this is a 1-d layout situation, which is what Flexbox is best at. Plus, I was pretty sure I could do it in two lines of CSS:

#controls-body {
    display: flex;
    justify-content: space-between;
}
Enter fullscreen mode Exit fullscreen mode

Done! Right?

... Nope.

While we do have three items, with one pulled left, another pulled right, and the other floating between them, you should see right away that the middle item is not centered properly; namely, instead of being centered on the page, it's centered between the other items. This would work fine if the items on the end are always the same size, but otherwise it's a no-go.

Another thing you might try is to not use justify-contents on the parent, but instead give all columns flex-grow: 1, then add some text-align rules to each. But this turns out to have exactly the same visual result:

Why don't these work? Well, we'll get to that in a minute. First, we'll look at something that does work.

Grid

Let's take the intuition from the second Flexbox attempt, to give each inline item its own "column" and make those "columns" the same size, and translate it over to CSS Grid, where we can drop the quotes: we'll literally define a column in a grid for each item and give them the same size using the fr unit introduced with Grid.

There are two ways to do this. The most obvious is to define three explicit columns:

#controls-body {
    grid-template-columns: 1fr 1fr 1fr;
}
Enter fullscreen mode Exit fullscreen mode

Nice and clean. But I like to future-proof my code, as long as it doesn't add much work or complexity. So instead of explicitly defining a set of columns, let's use Grid's auto-flow rules to handle any number of items, in case I want to add more controls to my demo later on:

#controls-body {
    grid-auto-flow: columns;  /* automatically place new items in new columns */
    grid-auto-columns: 1fr;   /* auto-columns should be 1fr wide */
}
Enter fullscreen mode Exit fullscreen mode

Combine these rules with the text-align rules above, and we get a winner:

So a grid with three 1fr columns works, but a flex container with three flex-grow: 1 items, or with justify-content: space-between, doesn't. Why? What's the difference?

The difference

So here's the thing. We often talk about Flexbox's flexy "distribute space evenly" rules, like flex-grow and justify-content: space-between, as being effectively the same as the fr unit introduced with CSS Grid. That works as an intuitive explainer of frs for devs who are familiar with Flexbox but new to Grid, but it's not really true. There's a subtle but very important difference that sometimes shows up, and it will explain the discrepancy here.

In Flexbox, space-between, flex-grow, etc. divide up the remaining space after all items are placed into the area. So if you have a 100px container with justify-content: space-between, as we do here, and three items of sizes 10px, 40px and 20px, Flexbox does the following calculation (roughly):

remainder = available space - total used space
          = 100px - (10px + 20px + 40px) = 100px - 70px
          = 30px
space between items = remainder / (# children - 1)
                    = 30px / (3-1) = 30px / 2
                    = 15px
Enter fullscreen mode Exit fullscreen mode

The resulting layout is:

10px item | 15px space | 40px item | 15px space | 20px item
Enter fullscreen mode Exit fullscreen mode

And the problem is that centering is not maintained. That 40px item's center point falls at 10px + 15px + (40px/2 = 20px), which is 45px. So it's 5 pixels off from the true center of the container, which is at 50px.

Grid's fr unit is different. It lays out the total space given to a grid item before considering the size of the item. So if we make our 100px container a grid-container with grid-template-columns: 1fr 1fr 1fr, Grid does the following calculation before it even considers the contents of the grid-items:

remainder = available space - (fixed size column widths)
          = 100px - 0
          = 100px
pixels per fr = remainder / (how many frs used across all column widths)
              = 100px / (1fr + 1fr + 1fr) = 100px / 3
              = 33.333px

So each `1fr` column gets 33.333px, an even third of the space. This means the center of the second column will fall at `33.333px + (half of 33.333px = 16.667px)`, which is a nice round `50px`!

> One very important thing that I totally didn't explain up there is the `fixed size column widths` variable, which in our case is `0`. This is the sum of any columns with values defined in a fixed unit, like `px`, `rem`, `in` (yeah, you can use inches in CSS! Nice for print layouts), or even `%`, which is fixed for the current size of the browser window. So for example if we used the rule `grid-template-columns: 30px 1fr 1fr`, then the calculation changes to this:
>```

python
remainder = available space - (fixed size column widths)
          = 100px - 30px
          = 70px
pixels per fr = remainder / (how many frs used across all column widths)
              = 70px / (1fr + 1fr) = 70px / 2
              = 35px


Enter fullscreen mode Exit fullscreen mode

So in this case, each 1fr column gets 35px.

Conclusion

Hopefully this gives some more insight into how Flexbox works, and gives someone who's still been holding out a reason to reconsider looking into Grid. It's shocking to me how many people I still encounter who are deeply skeptical of Grid.

As a final note, I want to be clear on something here: I am not saying that the fr unit is superior to Flexbox's behaviors. There are absolutely cases where Flexbox's behavior is exactly what you want. In many, many cases, it's more important to evenly divide the remaining space among siblings than to keep them all sitting in the same amount of space. Flexbox also allows for a lot more complexity around how items grow and shrink ("flex") based on the container. Maybe I'll write a thing about that at some point. Anyway, my point here is by no means to say that Grid's fr units are better than Flexbox, only that they are easier for this specific case. I still love Flexbox 💕💕💕


Endnote: tables

Sigh. Yes, yes, I know: you can do it with tables. Pretty cleanly, actually, especially if you use CSS tables instead of actual tables:


css
#controls-body {
  width: 100%;
  display: table;
  table-layout: fixed;
}
p {
  display: table-cell;
}


Enter fullscreen mode Exit fullscreen mode

That plus the text-align rules gives us another winner:

And honestly, this would have been much a better solution for me in 2012, even if I had to use actual tables because of limited browser support for CSS tables.

But here's my big problem with this. It's not just the oft-repeated, rarely-explained wisdom to "never use tables for layout" (although, seriously, now that we have Grid, never use tables for layout). This really is a decent enough solution in a pre-Grid context; it's clean, it's robust enough to handle adding extra items, and the code is readable, as long as you know what CSS tables are.

No, my problem comes in one word: responsiveness. Because it's not responsive, especially with an HTML table, and CSS tables aren't much better . Sure, you could drop the items into a single column for the mobile view. But suppose you had 4 columns, and wanted a 2x2 layout at an intermediate range. How would you do it with tables? You'd be hard pressed to do it without falling back to absolute positioning shenanigans again.

I won't say it's impossible; I can think of a couple options already, but they're not pretty, and they all have drawbacks. And most importantly, most, if not all, are specific to the current number of items, which makes for brittle code.

Grids, on the other hand, are amazingly responsive. It's trivial to redefine a grid's rows and columns, or to place things on a grid differently, so at minimum a media query breakpoint would be easy. (I wrote a whole article raving about Grid Areas, a feature that makes breakpoints that much more useful: CSS Grid Areas are amazing.) But in many cases, a breakpoint is unnecessary; Grid was built from the ground up with responsiveness in mind, and has many feature that support it, like repeat(auto-fill, ...) and minmax().

For more on all this, I highly recommend checking out Rachel Andrew's gridbyexample.com and Jen Simmons' Layout Land.

💖 💪 🙅 🚩
kenbellows
Ken Bellows

Posted on April 1, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related