You get a nice-looking design handed to you and it has this nice big hero section, followed by one of those three-up columns right beneath it. You know, like almost every other website you’ve ever worked on.
You bang through the hero section and get to work on the three-column section. It’s time to pull out our trusty friend flexbox! Except, you write display: flex
and you get this mess instead of getting the three equal columns you’d expect.
This happens because of how flexbox calculates the base size of an element. You’ve probably read lots of flexbox tutorials, and many of them (including my own) are an overly simplistic example of what flexbox does without really digging into the complex things that flexbox does for us that we take for granted.
I’m positive you’ve seen examples and tutorials that look at something like this, where three divs shrink down around the content that’s inside them:
In other words, we get some block-level elements shrinking down and slotting next to one another. It feels like flex wants things to be as small as possible. But in reality, flexbox actually wants things to be as big as possible.
Wait, what? Flex shrinks things by default — that can’t be right! Right?
As awesome as flexbox is, what it’s doing under the hood is actually a little strange because, by default, it is doing two things at once. It first looks at the content size which is what we would get if by declaring width: max-content
on an element. But on top of that, flex-shrink
is also doing some work allowing the items to be smaller, but only if needed.
To really understand what’s going on, let’s break those two down and see how they work together.
max-content
Diving into max-content
is a pretty handy property value in certain situations, but we want to understand how it works in this particular situation where we are trying to get three seemingly simple equal columns. So let’s strip away flexbox for a moment and look at what max-content
does on its own.
MDN does a good job of explaining it:
The
max-content
sizing keyword represents the intrinsic maximum width of the content. For text content this means that the content will not wrap at all even if it causes overflows.
Intrinsic might throw you off here, but it basically means we’re letting the content decide on the width, rather than us explicitly setting a set width. Uri Shaked aptly describes the behavior by saying “the browser pretends it has infinite space, and lays all the text in a single line while measuring its width.”
So, bottom line, max-content
allows the content itself to define the width of the element. If you have a paragraph, the width of that paragraph will be the text inside it without any line breaks. If it’s a long enough paragraph, then you end up with some horizontal scrolling.
Let’s revisit that overly-simplistic example of three block-level elements that shrink down and slot next to one another. That isn’t happening because of flex-shrink
; it’s happening because that’s the size of those elements when their declared width is max-content
. That’s literally as wide as they go because that’s as wide as the combined content inside each element.
Here, take a look at those elements without flexbox doing it’s flexbox stuff, but with a width: max-content
on there instead:
So, when there’s just a small amount of text, the intrinsic max-content
shrinks things down instead of flex-shrink
. Of course, flexbox also comes in with it’s default flex-direction: row
behavior which turns the flex items into columns, putting them right next to one another. Here’s another look but with the free space highlighted.
flex-shrink
to the equation
Adding So we see that declaring display: flex
pushes that max-content
intrinsic size on flex items. Those items want to be as big as their content. But there is another default that comes in here as well, which is flex-shrink
.
flex-shrink
is basically looking at all the flex items in a flexible container to make sure they don’t overflow the parent. If the flex items can all fit next to each other without overflowing the flexible container, then flex-shrink
won’t do anything at all… it’s job is already done.
But if the flex items do overflow the container (thanks to that max-content
intrinsic width thing), the flex items are allowed to shrink to prevent that overflow because flex-shrink
is looking out for that.
This is why flex-shrink
has a default value of 1
. Without it, things would get messy pretty quickly.
Here’s why the columns aren’t equal
Going back to our design scenario where we need three equal columns beneath a hero, we saw that the columns aren’t equal widths. That’s because flexbox starts by looking at the content size of each flex item before even thinking about shrinking them.
For simplicity’s sake, as we dive deeper into this, let’s work with some nice round numbers. We can do this by declaring widths on our flex items. When we declare a width on a flex item, we throw that intrinsic size out the window, as we’ve now declared an explicit value instead. This makes figuring out what’s really going on a lot easier.
In the Pen below, we have a parent that’s a 600px
wide flexible container (display: flex
). I’ve removed anything that might influence the numbers, so no gap
or padding
. I’ve also switched out the border
for an outline
so we can still visualize everything easily.
The first and third flex items have a width: 300px
and the middle one a width: 600px
. If we add that all up, it’s a total of 1200px
. That’s bigger than the the 600px
available within the parent, so flex-shrink
kicks in.
flex-shrink
is a ratio. If everything has the same flex-shrink
(which is 1
by default), they all shrink at the same rate. That doesn’t mean they all shrink to the same size or by the same amount, but they all shrink at the same rate.
If we jump back into Firefox DevTools, we can see the base size, the flex-shrink
and the final size. In this case, the two 300px
elements are now 150px
, and the 600px
one is now 300px
.
If we add up all the base sizes of all three flex items (the actual widths we declared on them), the total comes out to 1200px
. Our flex container is 600px
wide. If we divide everything by 2, it fits! They are all shrinking by the same rate, dividing their own widths by 2.
It’s not often that we have nice round numbers like that in the real world, but I think this does a nice job illustrating how flexbox does what it does when figuring out how big to make things.
Getting the columns to be equal
There are a few different ways to get the three columns we want to be equal in width, but some are better than others. For all the approaches, the basic idea is that we want to get all the columns base size to be the same. If they have an equal base size, then they will shrink (or grow, if we use flex-grow
) at an equal rate when flexbox does it’s flex things, and in theory, that should make them the same size.
There are a few common ways to do this, but as I discovered while diving into all of this, I have come to believe those approaches are flawed. Let’s look at two of the most common solutions that I see used in the wild, and I’ll explain why they don’t work.
flex: 1
Method 1: Using One way we can try to get all the flex items to have the same base size is by declaring flex: 1
on all of them:
.flex-parent { display: flex; }
.flex-parent > * { flex: 1; }
In a tutorial I made once, I used a different approach, and I must have had 100 people asking why I wasn’t using flex: 1
instead. I replied by saying I didn’t want to dive into the flex
shorthand property. But then I used flex: 1
in a new demo, and to my surprise, it didn’t work.
The columns weren’t equal.
The middle column here is larger than the other two. It’s not by a ton, but the whole design pattern I’m creating is just so you have perfectly equal columns every single time, regardless of the content.
So why didn’t it work in this situation? The culprit here is the padding
on the component in the middle column.
And maybe you’ll say it’s silly to add padding to one of the items and not the others, or that we can nest things (we’ll get to that). In my opinion, though, I should be able to have a layout that works regardless of the content that we’re putting in it. The web is all about components that we can plug and play these days, after all.
When I first set this up, I was sure it would work, and seeing this issue pop up made me want to learn what was really going on here.
The flex
shorthand does more than just set the flex-grow: 1
. If you don’t already know, flex
is shorthand for flex-grow
, flex-shrink
, and flex-basis
.
The default values for those constituent properties are:
.selector {
flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;
}
I’m not going to deep dive flex-basis
in this article, as that’s something Robin has already done well. For now, we can think of it like width
to keep things simple since we aren’t playing with flex-direction
.
We’ve already seen how flex-shrink
works. flex-grow
is the opposite. If the size of all the flex items along the main axis is smaller than the parent, they can grow to fill that space.
So by default, flex items:
- don’t grow;
- if they would otherwise overflow the parent, they are allowed to shrink;
- their width acts like
max-content
.
Putting that all together, the flex
shorthand defaults to:
.selector {
flex: 0 1 auto;
}
The fun thing with the flex
shorthand is you can omit values. So, when we declare flex: 1
, it’s setting the first value, flex-grow
, to 1
, which basically turns on flex-grow
.
The strange thing here is what happens to the values that you omit. You’d expect them to stay at their defaults, but they don’t. Before we get to what happens, though, let’s first dive into what flex-grow
even does.
As we’ve seen, flex-shrink
allows elements to shrink if their base sizes add up to a computed value that’s bigger than the available width of the parent container. flex-grow
is the opposite. If the grand total of the element base sizes is smaller than the value of the parent container’s width, then they will grow to fill the available space.
If we take that super basic example where we have three small divs next to one another and add flex-grow: 1
, they grow to fill that leftover space.
But if we have three divs with unequal widths — like those ones we started with — adding flex-grow
to them won’t do anything at all. They won’t grow because they’re already taking up all the available space —so much space, in fact, that flex-shrink
needs to kick in and shrink them down to fit!
But, as folks have pointed out to me, setting flex: 1
can work to create equal columns. Well, sort of, as we saw above! In simple situations it does work though, as you can see below.
When we declare flex: 1
it works because, not only does this set the flex-grow
to 1
, but it also changes the flex-basis
!
.selector {
flex: 1;
/* flex-grow: 1; */
/* flex-shrink: 1; */
/* flex-basis: 0%; Wait what? */
}
Yup, setting flex: 1
sets the flex-basis
to 0%
. This overwrites that intrinsic sizing we had before that behaved like max-content
. All of our flex-items now want to have a base size of 0
!
So their base sizes are 0
now, but because of the flex-grow
, they can all grow to fill up the empty space. And really, in this case, flex-shrink
is no longer doing anything, as all the flex items now have a width of 0
, and are growing to fill the available space.
Just like the shrink example before, we’re taking the space that’s available, and letting all the flex items grow at an equal rate. Since they are all a base width of 0
, growing at an equal rate means the available space is equally divided between them and they all have the same final size!
Except, as we saw, that’s not always the case…
The reason for this is because, when flexbox does all this stuff and distributes the space, whether it’s shrinking or growing a flex item, it’s looking at the content size of the element. If you remember back to the box model, we have the content size itself, then the padding
, border
, and margin
outside of that.
And no, I didn’t forget * { box-sizing: border-box; }
.
This is one of those strange quirks of CSS but it does make sense. If the browser looked at the width of those elements and included their padding
and borders
in the calculations, how could it shrink things down? Either the padding
would also have to shrink or things are going to overflow the space. Both of those situations are terrible, so instead of looking at the box-size
of elements when calculating the base size of them, it only looks at the content-box!
So, setting flex: 1
causes a problem in cases where you have border
s or padding
on some of your elements. I now have three elements that have a content-size of 0
, but my middle one has padding on it. If we didn’t have that padding, we’d have the same math we did when we looked at how flex-shrink
works.
A parent that is 600px
and three flex items with a width of 0px
. They all have a flex-grow: 1
so they grow at an equal rate, and they each end up 200px
wide. But the padding mucks it all up. Instead, I end up with three divs with a content size of 0
, but the middle one has padding: 1rem
. That means it has a content size of 0
, plus 32px
padding as well.
We have 600 - 32 = 568px
to divide equally, instead of 600px
. All the divs want to grow at an equal rate, so 568 / 3 = 189.3333px
.
And that’s what happens!
But… remember, that’s their content size, not the total width! That leaves us with two divs with a width of 189.333px
, and another with a which of 189.333px + 32 = 221.333px
. That’s a pretty substantial difference!
flex-basis: 100%
Method 2: I have always handled this like this:
.flex-parent {
display: flex;
}
.flex-parent > * {
flex-basis: 100%;
}
I thought this worked for the longest time. Actually, this was supposed to be my final solution after showing that flex: 1
doesn’t work. But while I was writing this article, I realized it also falls into the same problem, but it’s a lot less obvious. Enough so that I didn’t notice it with my eyes.
The elements are all trying to be 100%
width, meaning they all want to match the width of the parent, which, again, is 600px
(and in normal circumstances, is often much bigger, which further reduces the perceivable difference).
The thing is that 100%
includes the padding in the computed values (because of * { box-size: border-box; }
, which for the sake of this article, I’m assuming everyone is using). So, the outer divs end up with a content size of 600px
, whereas the middle div ends up with a content size of 600 - 32 = 568px
.
When the browser is working out how to evenly divide the space, it isn’t looking at how to evenly squish 1800px
into a 600px
space, but rather it’s looking at how to squish 1768px
. Plus, as we saw earlier, flex items don’t shrink by the same amount, but at an equal pace! So, the element with padding shrinks slightly less in total than the others do.
This results in the .card
having a final width of 214.483px
while the others clock in at 192.75px
. Again, this leads to unequal width values, though the difference is smaller than we saw with the flex: 1
solution.
Why CSS Grid is the better choice here
While all this is a little frustrating (and even a little confusing), it all happens for a good reason. If margins, padding, or borders changed sizes when flex gets involved, it would be a nightmare.
And maybe this means that CSS Grid might be a better solution to this really common design pattern.
I’ve long thought that flexbox was easier to pick up and start using than grid, but grid gives you more ultimate control in the long run, but that it’s a lot harder to figure out. I’ve changed my mind on that recently though, and I think not only does grid give us better control in this type of situation, but it’s actually more intuitive as well.
Normally, when we use grid, we explicitly declare our columns using grid-template-columns
. We don’t have to do that though. We can make it behave a lot like flexbox does by using grid-auto-flow: column
.
.grid-container {
display: grid;
grid-auto-flow: column;
}
Just like that, we end up with the same type of behavior as throwing display: flex
on there. Like flex
, the columns can potentially be unbalanced, but the advantage with grid is that the parent has total control over everything. So, rather than the content of the items having an impact like they would in flexbox, we only need one more line of code and we can solve the problem:
.grid-container {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
}
I love that this is all on the parent selector, and that we don’t have to select the children to help get the layout that we are after!
The interesting thing here is how fr
units work. They are literally called “flex units” in the spec, and they work just like flexbox does in dividing up space;. The big difference: they’re looking at the other tracks to determine how much space they have, paying no attention to the content inside those tracks.
That’s the real magic here. By declaring grid-auto-columns: 1fr
, we are in essence saying, “by default, all my columns should have an equal width,” which is what we’ve been after from the start!
But what about at small screens?
What I love with this approach is we can keep it super simple:
.grid-container {
display: grid;
gap: 1em;
}
@media (min-width: 35em) {
grid-auto-flow: column;
grid-auto-columns: 1fr;
}
And just like that, it works perfectly. And by declaring display: grid
from the start, we can include the gap
to maintain equal spacing, whether the children are rows or columns.
I also find this to be a lot more intuitive than changing the flex-direction
within a media query to get a similar result. As much as I love flexbox (and I really do still think it has great use cases), the fact that declaring flex-direction: column
creates rows, and vice versa, is a little counter-intuitive at first glance.
And of course, if you prefer rolling without media queries, there is nothing stopping you from taking this to the next level with the help of auto-fit
, which would be similar to setting something up with flex-wrap
(though not exactly the same):
.grid-container {
display: grid;
gap: 1em;
grid-template-columns: repeat(auto-fit, minmax(10em, 25em));
}
Making it work with flexbox
I realize that we can get around this with flexbox by nesting the element with the padding
on it. We’ve done this since we started making layouts using floats, and Bootstrap really hammered home this type of design pattern.
<div class="container">
<div class="row">
<div class="col"> <div class="">... </div>
<div class="col"> <div class="element-with-padding">...</div> </div>
<div class="col"> ... </div>
</div>
</div>
And there is nothing wrong with that. It works! But floats work too, and we’ve stopped using them for layouts as better solutions have been released in recent years.
One of the reasons that I love grid is because we can simplify our markup quite a bit. Just like we ditched floats for a better solution, I’d at least like to people to keep an open mind that maybe, just maybe, grid could be a better, and more intuitive solution for this type of design pattern.
Flexbox still has it’s place, of course
I still love flexbox, but I think its real power comes from times that we want to rely on the intrinsic width of the flex items, such as with navigations, or groups of elements, such as buttons or other items of varying width that you want to go next to one another.
In those situations, that behavior is an asset that makes things like even spacing between unequal items such a breeze! Flexbox is wonderful, and I have no plans to stop using.
In other situations though, when you find yourself fighting with how flexbox is trying to work, maybe you could turn to grid instead, even if it’s not a typical “2d” grid where you’re told you should be using it for.
People often tell me that they struggle to figure out grid because it’s too complicated, and while it can be, as we saw here, it doesn’t have to be.
Awesome write-up. Thanks.
Like you, I reach for
grid-template-columns: repeat(auto-fit, minmax(10em, 25em));
to solve this type of problem.Yes!
auto-fit
withminmax()
is so great, still feels like magic every time I use it!Have you tried using the
columns
CSS property (and other related properties) yet?I think
columns
is one of the most underused properties, and for some things it’s fantastic! But with how it works, it is a little fringe in the use cases… which I guess is why it’s underused, heh.Great article! This part I don’t quite comprehend though – “declaring
flex-direction: column
creates rows, and vice versa”If you declare
flex-direction: row
on the parent, the children become columns :)IMHO, the thing is that Flexbox and Grid each have a slightly different meaning/mental model.
Flexbox says “I’m set to column, so I will be a column.”
This makes perfect sense, as Flexbox is a “1D” layout.
Grid says “I’m set to column, so I will make all my children columns.”
Again, this makes perfect sense, as Grid is a 2D layout.
Thanks for a great article! Much appreciated :)
I noticed while reading this on an iPhone, that the Grid version isn’t showing equal width columns.
To really make sure that the columns have equal sizes, you could say
grid-auto-columns: minmax(0, 1fr)
.Oh, thanks for pointing that out! Yeah, the 1fr is distributing the space, but it won’t go below
min-width
in size, so each column will go to the width of the longest word. Letting it reach0
, allows them to keep shrinking… though I think ideally we don’t have columns in that case at all anymore :DExcellent article on some of the weirdness and power of flexbox.
I always tend to use:
Any reasons why that shouldn’t work in all situations?
That works! They aren’t shrinking or growing, but are just going by the size you’ve given them. The reason I don’t like that as a solution though, is the code is more “fragile”, in that it’s completely dependent on how many children are nested in there.
For me, ideally, this is more of a utility class that I can use to get even columns regardless of how many columns that results in, but also, layouts often change, and if you went from 3 to 4 columns with an update to the design, it sucks to have to rewrite the CSS to get it to work. It’s not the end of the world and should be easy enough to fix, but if there are ways that I can do the same thing and not have to update the CSS (or change the class used in the HTML), all the better IMO :D
@Kevin Powell To play devil’s advocate. The design shows 3 columns which would be 33.33% so the code wouldn’t be fragile in my opinion. It would be 33.33% except on mobile. Just like 4 would be 25% etc.
Can you expand on fragile?
flex: 1 0 33%
on the child and it’s done? Am I missing something?I don’t like using set percentages, because then if I update things and it’s 4 children, or 2 children, I need to update more than just throwing a new component in there and it working… using the grid approach, I can re-use that anywhere, no matter how many children and how they’re set up. One class that works, instead of a handful, makes life a lot easier :)
It’s a bit of a magic number, but
flex-basis: 33.33%
could get the job done.As you said though, it’s a bit of a magic number. I should have used some examples with 2 or 4 columns as well.
I like the idea of having one class that can handle any situation (when possible), rather than a set of classes and picking the right one when needed, and changing it if the content is updated :)
In Webkit, I found that adding a border to the flex parent sometimes created a gap that the children would not fill, no matter what. Freaking Webkit… Switching to grid saved it.
Hi Kevin, thank you for great article. Let’s say I have 4 columns. Every column has a different words and their lengths are not equal. First word is ‘a’, second word is ‘bb’, third word is ‘ccc’ and the last word is ‘zzzzzzzzzzzzzzzzz’. I want to make all columns equal size. I am saying 4 columns but it can change. How can I figure this thing out?
People that are suggesting 33.333% are missing the point. When the author responds with fragility he just means that he wants things to work dynamically, because it’s not rare that we need to add another column for feature x or remove a column because it’s no longer relevant.
I think the best way to set “fixed” width for flex-items is set min-width (min-width: 50%, 25% …etc) for this ones, this will overide effect case by flex-grow or flex-shrink
But what if i want to use flex-basis so my columns wraps without @media queries?
When columns wrapper larger than 60rem – they are equal. When wrapper width between 50 and 60 rem – column2 shrinks. When wrapper smaller than 50rem they are wrapped.
I solved it by setting
but it seems more like a hack. Is there “right” solution for this kind of problem?
Awesome explanation! Got a huge lot to consider when creating my designs. I fully agree that Flex and Grid serve different goals and have different mental models. When I think of Grid, grid templates come to mind, which take my fascination even further. Flex is my favorite when I want to justify and align items, rows and columns all over the place, not so much for creating repeating patterns. Thanks for the pleasure of reading your article, Kevin!
flex-basis: 30%;
set it on the children – works even when using a gap.