Masonry layouts using CSS Grid
How to get a great looking gap-less layout using CSS Grid.
Masonry Layout
The idea of a masonry grid is that different blocks of content can have a variety of sizes and that they all interconnect beautifully to create a stunning layout. A great example of this can be found in the Pinterest app where content simply fits on the screen and allows you to infinitely scroll through the data.
Why is this hard?
For the longest time this has been incredibly difficult to build well using just CSS.
Whilst at first glance it may look like two columns next to each other, the challenge comes when trying to figure out what data goes in each column.
You're effectively doing something akin to the following to decide what data goes in which column, which is far from a CSS only solution.
Please note:
grid-template-rows
andgrid-template-columns
may eventually support themasonry
property however it's currently only available behind a Firefox feature flag. This article discusses a cross browser approximation.
const column1 = data.filter( (_, index) => index % 2 === 0 ); const column2 = data.filter( (_, index) => index % 2 !== 0 );
Enter CSS Grid
CSS grid is a powerful and relatively new feature which allows us to easily create tabular and grid style layouts.
We can easily configure the grid layout on a single parent element, and all children are sized and aligned accordingly.
.grid { max-width: 1024px; margin: 16px auto; display: grid; gap: 16px; grid-auto-flow: dense; } @media (min-width: 48em) { .grid { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 64em) { .grid { grid-template-columns: repeat(3, 1fr); } }
Spanning columns and rows
Now we've got our simple grid, it's down to our individual cells to tell the grid who and what they are.
So if we consider our grid above, and for ease let's just talk about the largest layout, where we have a 3 column grid. Let's say that we want to (using Excel terms) "merge" two cells, we can do that quite easily in the css by using either the grid-column
or row-column
properties.
.block { background: linear-gradient(90deg, rgba(63, 94, 251, 1) 0%, rgba(252, 70, 107, 1) 100%); min-height: 300px; } .block--wide { grid-column: span 2; background: linear-gradient(90deg, rgba(131, 58, 180, 1) 0%, rgba(253, 29, 29, 1) 50%, rgba(252, 176, 69, 1) 100%); } .block--tall { grid-row: span 2; background: linear-gradient(0deg, rgba(34,193,195,1) 0%, rgba(253,187,45,1) 100%); }
Initially these properties allow us to tell the grid where to place our cell, such as: grid-column: 1
which regardless of row would place our content in the first column, however, instead of using such fixed terms we can introduce the powerful span
value into the equation such as grid-column: span 2
which would configure our cell to span 2 columns. Equally we could also use grid-row: span 2
which would configure our cell to span 2 rows.
Misaligned Content
Hopefully we can see that our masonry grid is starting to take shape, but all we've done is substituted our initial problem of splitting content across multiple columns, into a problem of making sure we haven't misaligned our cells.
For example, if we revert to our original grid, and had our third cell spanning two columns, CSS grid would wrap it onto the next line thus creating a gap.
Plugging the gap
Thankfully CSS grid has a great solution for this: grid-auto-flow: dense
.
This tells the grid to try and fill gaps with the next available cell that will fit. Of course we still need to ensure that we're providing enough smaller cells to resolve the problem.
.grid { max-width: 1024px; margin: 16px auto; display: grid; gap: 16px; grid-auto-flow: dense; } @media (min-width: 48em) { .grid { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 64em) { .grid { grid-template-columns: repeat(3, 1fr); } }
As we can see in the image below, the gap has been automatically filled.
Whilst this approach will slightly change the visual order of the content, the tab order (and screen reader order) will remain the same.
Final Solution
Personally i find this a great approach as it removes the problem of having to compute what content goes where, as it's no longer column dependent; cells simply fit in as best they can, even if the grid composition changes between breakpoints.
We can even mix and match spanning columns and rows to create some truly unique layouts!
.grid { max-width: 1024px; margin: 16px auto; display: grid; gap: 16px; grid-auto-flow: dense; } @media (min-width: 48em) { .grid { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 64em) { .grid { grid-template-columns: repeat(3, 1fr); } } .block { background: linear-gradient(90deg, rgba(63, 94, 251, 1) 0%, rgba(252, 70, 107, 1) 100%); min-height: 300px; } .block--tall { grid-row: span 2; background: linear-gradient(0deg, rgba(34,193,195,1) 0%, rgba(253,187,45,1) 100%); } .block--taller { grid-row: span 2; background: linear-gradient(90deg, rgba(131, 58, 180, 1) 0%, rgba(253, 29, 29, 1) 50%, rgba(252, 176, 69, 1) 100%); }
<div class="grid"> <div class="block block--tall"></div> <div class="block"></div> <div class="block"></div> <div class="block block--taller"></div> <div class="block"></div> <div class="block"></div> <div class="block block--tall"></div> <div class="block block--tall"></div> <div class="block"></div> <div class="block"></div> <div class="block"></div> </div>
See the full code example on github:
geometricpanda/blog-masonry-grid