Responsive Multi-Column Lists with Flexbox
Note: This post originally appears on fourword.fourkitchens.com
Vertical Centering.
Those two simple words used to bring fear and trepidation to anyone having deal with the shortcomings of vertical-align: middle
. Thankfully, Flexbox has saved the day in that regard.
If all Flexbox brought us was sane vertical centering, I'd be more than overjoyed with it. Yet it brings many other capabilities to the table, such that I think it's one of the biggest advancements we've had on the front-end for quite some time. Even-height columns, layout re-ordering, sticky footers on short pages; all things that Flexbox makes dead simple (except for, as usual, a few browser bugs here and there).
Yet there are features under the hood of flexbox that do more than just make it easier to do what we've already done. On a recent project, I've had some fun using flexbox to create a responsive, multi-column list that's easy to set up and flexible with various size content.
Listing Names
The task was fairly simple: with a list of N items, display them in X columns that grow and shrink with the container width (where N is a number dependent on number of results and X is a number dependent on screen width).
The HTML is straight-forward:
<div class="items">
<div class="item">Heriberto Nickel</div>
<div class="item">Brittaney Haliburton</div>
<div class="item">Maritza Winkler</div>
<div class="item">Carmon Rigg</div>
<div class="item">Alice Marmon</div>
<div class="item">Lyman Steakley</div>
<div class="item">Zenia Correa</div>
</div>
Using floats, the CSS for this would be:
.items {
overflow: hidden; /* simple clearfix */
}
.items .item {
float: left;
width: 25%;
}
This gives us four columns that wrap. We can also add a little bit of style to give it a more pleasing look:
.items .item {
float: left;
width: 25%;
box-sizing: border-box;
background: #e0ddd5;
color: #171e42;
padding: 10px;
}
That works well enough, if you have content that's exactly the same. No one has content that's exactly the same. This is where floats get us in trouble. Some of our names are longer than others, and at a certain widths, they wrap while others don't:
Listing Names with Flexbox
Flexbox takes care of this, though, by always ensuring even-height columns despite uneven content. Here's what the CSS looks like for it:
.items {
display: flex;
flex-wrap: wrap;
}
.items .item {
flex: 1 0 25%;
box-sizing: border-box;
background: #e0ddd5;
color: #171e42;
padding: 10px;
}
I'm going to assume right now that you're already familiar with flexbox and how this works. If not, go check out CSS Trick's Complete Guide to Flexbox for a great intro.
Browser Support
I should probably get this out of the way: this won't work if you have to fully support IE9 or below. Flexbox in general just doesn't work there.
In many instances (where usage is <1%), the fallback of a single column of items is an acceptable compromise (I've gone this route for a recent project). The content still shows fine, just not as concise.
For more details on flexbox browser support, check out the caniuse.com flexbox page.
Also, because Safari and IE10 require browser prefixes, I recommend using a tool like Autoprefixer to help out.
Spacing Out
All of our content is smushed together, leaving out some precious whitespace. Adding a margin-right
to our items would add that space, but also causes the last item in every row to have a right margin that just isn't needed.
Using :last-child
won't work, since the last item in a column could be items 4 and 8 of 9 total. We really need :last-column-in-this-row
but I don't think that selector exists.
Instead, we use a simple workaround to achieve our desired result. We add a left/top margin to all of our items, then adjust for that extra space in our container, like so:
.items {
margin-left: -10px;
margin-top: -10px;
}
.items .item {
flex: 1 0 calc(25% - 10px);
margin-left: 10px;
margin-top: 10px;
}
Responsive Grids
Now we have columns that wrap nicely and maintain a consistent height, but it's not very responsive. It always sticks at 4 columns, even if it might fit better to shrink to two or even just one (and even if the content doesn't fit).
Normally we'd turn to some breakpoints to change the flex-basis
property. While we could do that, there's another technique worth trying.
Instead of saying we want a flex-basis
at some percentage of the container, we can ignore the container and instead define the minimum width allowed for the content.
flex: 1 0 200px;
Now we have columns that grow/shrink in number depending on what fits, without resorting to any media queries.
That Last Row
One interesting side effect of this is that when the number of items isn't evenly divisible by the number of columns, the width of the items in the last row doesn't match the widths of all the previous rows:
This is because flexbox expands the columns in that row so combined they fill 100% of the width. Some people may enjoy this look, but it can look like a bug (especially if the design doesn't include an item background):
The one way I know to solve this requires some media queries. Depending on the screen width, we add a max-width
to the item that matches the percent width of the columns minus the gutter (so if you had 5 columns with a 10px gutter, the max-width
would be calc(20% - 10px)
).
In our example, to get our breakpoints, we'll use multiples of the min-width. We also have to factor in the gutter for Safari, otherwise we can end up with a max-width
being set before the column number changes.
We have a min-width
of 200px
, so we want breakpoints at 410px
(2x200px + 1x10px), 620px
(3x200px + 2x10px), 830px
(4x200px + 3x10px) and so on:
@media (min-width: 410px) {
.items .item {
max-width: calc(50% - 10px);
}
}
@media (min-width: 620px) {
.items .item {
max-width: calc(33.33333% - 10px);
}
}
@media (min-width: 830px) {
.items .item {
max-width: calc(25% - 10px);
}
}
This limits the width of the items in the final row and brings them inline with the above content.
This doesn't pay attention to the list container width, so if your container is skinnier than your screen, you need to adjust your breakpoints accordingly. It's messy; unfortunately I don't know of a way around it.
Making it a Sass Mixin
If you enjoy this method and want to use it in various scenarios, chances are your min-width and gutter will vary per use. While you can copy/paste and adjust the variables as needed, that's not very efficient.
To help out, I've written a Sass mixin that does the heavy lifting. It will fill out the min-width, gutter, and figure out your media queries for you.
View Mixin (with example code)
You can even define whether you'd like the final row to match widths with the previous rows.
I've also added a feature to set a max number of columns to display. At that max, the items will continuously grow to fill the extra space, and no more columns will be added. This prevents you from having to include 20 breakpoints for super wide screens.
Wrapping Up
Flexbox brings powerful new features to layout and with that, we're get to be creative with new solutions to old problems. What other new techniques have you seen flexbox tackle that haven't seen much press yet?