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.

A screenshot of the Pinterest app

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 and grid-template-columns may eventually support the masonry property however it's currently only available behind a Firefox feature flag. This article discusses a cross browser approximation.

js
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.

css
.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);
    }
}
CSS grid of two columns across three rows, containing square gradientsCSS grid of one column across three rows, containing square gradientsCSS grid containing three columns across two rows of square gradients

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.

css
.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.

Layout of 6 gradient squares, the 2nd, 3rd and 6th span 2 columns eachCSS grid layout of gradient squares with the first and 4th cell spanning 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.

CSS grid showing 8 gradient cells, the third cell spans two columns and has been wrapped to the next line 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.

css
.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.

CSS grid with 8 cells, the fourth cell is spanning two columns

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!

CSS Masonry grid containing multiple cells with a variety of gradient coloursCSS grid with 6 gradient cells, the first cell spans two rows, the fourth and fifth cells span two columns each
CSS Solution
css
.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%);
}
HTML Solution
html
<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