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 fr
s 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:
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;
}
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;
}
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 */
}
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 fr
s 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
The resulting layout is:
10px item | 15px space | 40px item | 15px space | 20px item
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
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;
}
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.
Top comments (3)
Great article!
If I'm not missing something you can achieve the same thing with flexbox by changing flex-grow: 1 to flex: 1
codepen.io/kenbellows/pen/drwagy
flex: 1 would implicitly set flex-basis to 0 (instead of default auto) so the available space would be distributed evenly between flex items (like in the grid example below)
Thanks for explaining that limitation of flex layouts, I can understand better how flex mode works now.
Thank You!!!