CSS masonry with flexbox, :nth-child(), and order

On the surface it seems fairly easy to create a masonry layout with flexbox; all you need to do is set flex-flow to column wrap and voilà, you have a masonry layout. Sort of. The problem with this approach is that it produces a grid with a seemingly shuffled and obscure order. Items will be (unbeknownst to the user) rendered from top to bottom and someone parsing the grid from left to right will read the boxes in a somewhat arbitrary order, for example 1, 3, 6, 2, 4, 7, 8, 5, and so on so forth.

Flexbox has no easy way of rendering items with a column layout while using a row order, but we can build a masonry layout with CSS only—no JavaScript needed—by using :nth-child() and the order property. In a gist, here’s the trick to create a row order while using flex-direction: column, given that you’re rendering three columns:

/* Render items as columns */
.container {
  display: flex;
  flex-flow: column wrap;
}

/* Re-order items into rows */
.item:nth-child(3n+1) { order: 1; }
.item:nth-child(3n+2) { order: 2; }
.item:nth-child(3n)   { order: 3; }

/* Force new columns */
.container::before,
.container::after {
  content: "";
  flex-basis: 100%;
  width: 0;
  order: 2;
}

This will create a masonry layout with items rendered as columns but ordered as rows (the gray vertical lines represent the pseudo elements that force line breaks):

1
2
3
4
5
6
7
8
9
10

Let’s break down the problem (or jump to the solution).

Pick your poison: a shuffled order, or weird gaps

Flexbox is not really built for masonry—if you set a fixed height on a flex container (so that items can wrap when they overflow) and flex-flow to column wrap, you’ll achieve something like this:

1
2
3
4
5
6
7
8
9
10

Items are rendered in columns from top to bottom, producing an arbitrary order when read from left to right. This is of course the expected outcome and desirable in many scenarios, but not when we’re trying to create a masonry layout, and it becomes increasingly disorienting as a page grows taller.

If we instead change the flex-direction to row and have elements of varying heights we’ll achieve the correct order but with weird and unexpected gaps all over our grid:

1
2
3
4
5
6
7
8
9
10

So it seems impossible to get the best of both worlds: items rendered as columns but ordered as rows. You might decide to use flex-direction: column and just move around the elements in your HTML to achieve the right visual order, but this can be cumbersome, it’s unnecessarily complex, and it will mess up the tab order of the elements.

Re-ordering elements with order and nth-child()

The order property affects the order of items contained in a CSS flexbox or grid, and we can use it to re-order items for our soon-to-be masonry layout. The order property is pretty straight-forward to use: if you have two elements and one has order: 1 and the other one has order: 2 the element with order: 1 will be rendered before the other element, independent of their HTML source code order.

The CSS masonry solution depends on a detail of the order specification: what happens if two or more elements have the same order value? Which comes first? Flexbox falls back on the source code order: the element that appears first in the source code will be rendered before other elements with the same order value. This fact gives us the possibility to easily re-group items in our grid so that we can change the ordering from columns to rows, while still rendering those rows as columns, using nth-child().

Look at the table below. To achieve a sensible order using flex-direction: row we’d just have to render elements in the default order: 1, 2, 3, 4, 5, 6 , etc.

  Column 1 Column 2 Column 3
Row 1 1 2 3
Row 2 4 5 6
Row 3 7 8 9
Row 4 10 11 12

If we want to achieve the same order while using flex-direction: column we need to change the order of the elements to match the order of each column in the table (rather than each row):

  Column 1 Column 2 Column 3
Row 1 1 2 3
Row 2 4 5 6
Row 3 7 8 9
Row 4 10 11 12

I.e. the first elements in our flexbox layout have to be 1, 4, 7, 10. These items will fill up the first column, followed by 2, 5, 8, 11 for the 2nd column and 3, 6, 9, 12 for the 3rd and last column. This is where the nth-child() selector comes in. We can use it to select every third element (3n), starting with the first element (3n+1), and set all those elements to have the same order value:

.item:nth-child(3n+1) { order: 1; }

This selector sets order: 1 for element 1, 4, 7, 10 in our container, i.e. the entire first column. In other words we’re using a combination of nth-child() and order to re-order items depending on their original order. To create the 2nd and 3rd column we just change the offset:

.item:nth-child(3n+1) { order: 1; }
.item:nth-child(3n+2) { order: 2; }
.item:nth-child(3n) { order: 3; }

Here we’re producing three sets: 1, 4, 7, 10 (3n+1) with order: 1, 2, 5, 8, 11 (3n+2) with order: 2, and 3, 6, 9, 12 (3n) with order: 3. All together the order becomes 1, 4, 7, 10, 2, 5, 8, 11, 3, 6, 9, 12.

If we make sure to render each of those groups as one column (and no more), it’ll create the illusion that the items have returned to their original order when you read from left to right. If we visually parse the grid as rows the first row will contain the first element from every group (1, 2, 3), the second row will contain the second element from every group (4, 5, 6), and so on so forth. With this technique we can create a masonry layout with items rendered as columns but ordered as rows.

1
2
3
4
5
6
7
8
9
10

How is the tab order affected by shuffling around elements like this? Luckily, not at all. order only changes the visual representation of objects, not the tab order, so tabbing through the grid will work as intended.

Preventing columns from merging

If you have many items in a masonry layout this technique will fairly certainly break down at some point. We’re counting on that every “group” that we’ve created will be rendered as exactly one column but in reality items can have different heights and columns can easily start to merge. The first column could be longer than the other two, for example, which could make the third column start at the end of the second column:

1
2
3
4
5
6
7
8
9
10

The highlighted box here (3) has to be rendered at the start of the third column or the ordering algorithm will break, but if there’s space for another element at the end of the 2nd column it will naturally be rendered there.

We can fix this wrapping issue by forcing columns to restart at certain points. There’s no easy way of saying “this element should line break” with flexbox, but we can achieve this effect by adding invisible elements that take up 100% of the container’s height. As they require 100% of the parent’s height to be rendered they won’t fit in a column together with any other element, so they’ll essentially force line breaks by creating collapsed columns.

We have to insert these line break elements into our grid and array of elements, so what we’re looking for is to create this sequence of elements: 1, 4, 7, 10, <break>, 2, 5, 8, 11, <break>, 3, 6, 9, 12. We can use pseudo-elements on the container to add these, and we can set the order to 2 on both of them. Adding a pseudo-element with :before will make it the first child of the container and adding a pseudo-element with :after will make it the last child of the container, so if we set order: 2 on both of them they will become the first and the last element of the order: 2 “group” (as they appear before and after the other elements): :before, 2, 5, 8, 11, :after.

/* Force new columns */
.container::before,
.container::after {
  content: "";
  flex-basis: 100%;
  width: 0;
  order: 2;
}

I’ve highlighted the pseudo-elements below to show their effect. Notice that despite that box 3 would fit in the 2nd column it’s rendered as the first element in the last column:

1
2
3
4
5
6
7
8
9
10

The solution

As a final step, you need to make sure that your flex container has a set height that makes it taller than your tallest column (so that all columns will fit). Put together, this will produce a CSS masonry layout with three columns:

.container {
  display: flex;
  flex-flow: column wrap;
  align-content: space-between;
  /* Your container needs a fixed height, and it 
   * needs to be taller than your tallest column. */
  height: 600px; 
}

.item {
  width: 32%;
  margin-bottom: 2%; /* Optional */
}

/* Re-order items into 3 rows */
.item:nth-child(3n+1) { order: 1; }
.item:nth-child(3n+2) { order: 2; }
.item:nth-child(3n)   { order: 3; }

/* Force new columns */
.container::before,
.container::after {
  content: "";
  flex-basis: 100%;
  width: 0;
  order: 2;
}

Your HTML should look like this, with one <div /> for every item in your grid:

<div class="container">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  ...
</div>

Working with more than three columns

To create a masonry layout with more than three columns we need to do three things: adapt our sorting algorithm, update the width of the items, and insert line break elements manually (instead of using pseudo-elements). For the sorting, we just need to change the number of “groups” that we create with the nth-child() and order combination. You can create four columns like so:

.item:nth-child(4n+1) { order: 1; }
.item:nth-child(4n+2) { order: 2; }
.item:nth-child(4n+3) { order: 3; }
.item:nth-child(4n)   { order: 4; }

Since we’re limited to creating just two pseudo-elements with :before and :after we need to manually add the line break elements into our container instead of relying on pseudo-elements. They’ll be automatically sorted into the right places (we need one line break element less than we have columns, as the last column doesn’t have to break):

<div class="container">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  ...
  <div class="item break"></div>
  <div class="item break"></div>
  <div class="item break"></div>
</div>

Update the line break CSS to target these elements:

/* Force new columns */
.break {
  content: "";
  flex-basis: 100%;
  width: 0;
  margin: 0;
}

This will create a masonry layout with four columns:

1
2
3
4
5
6
7
8
9
10
11
12

Here’s the full solution for a CSS masonry layout with four columns:

.container {
  display: flex;
  flex-flow: column wrap;
  align-content: space-between;
  /* Your container needs a fixed height, and it 
   * needs to be taller than your tallest column. */
  height: 600px; 
}

.item {
  width:24%;
  margin-bottom: 2%; /* Optional */
}

/* Re-order items into 4 rows */
.item:nth-child(4n+1) { order: 1; }
.item:nth-child(4n+2) { order: 2; }
.item:nth-child(4n+3) { order: 3; }
.item:nth-child(4n)   { order: 4; }

/* Force new columns */
.break {
  content: "";
  flex-basis: 100%;
  width: 0;
  margin: 0;
}

This CSS-only way of creating a masonry (or mosaic) layout is surely not as robust, flexible, and foolproof as a JavaScript implementation (like Masonry) but if you don’t want to rely on a third-party library just to create a masonry layout I hope that these layouts tricks can come in handy.

For help with more common CSS flexbox patterns, I’ve compiled a list of flexbox examples that you can copy and paste into your projects.

Published in tutorialcss | 15 Apr 2019