
Making a Masonry Layout That Works Today
Making a Masonry Layout That Works Today êŽë š
Many CSS experts have weighed heavily on possible syntaxes for a new masonry layout feature last year. There were two main camps and a third camp that strikes a balance between the two:
- Use
display: masonry
- Use
grid-template-rows: masonry
- Use
item-pack: collapse
I donât think theyâve came up with a resolution yet. But you might want to know that Firefox already supports the masonry layout with the second syntax. And Chrome is testing it with the first syntax. While itâs cool to see native support for CSS Masonry evolving, we canât really use it in production if other browsers donât support the same implementationâŠ
So, instead of adding my voice to one of those camps, I went on to figure out how make masonry work today with other browsers. Iâm happy to report Iâve found a way â and, bonus! â that support can be provided with only 66 lines of JavaScript.
In this article, Iâm gonna show you how it works. But first, hereâs a demo for you to play with, just to prove that Iâm not spewing nonsense. Note that thereâs gonna be a slight delay since weâre waiting for an image to load first. If youâre placing a masonry at the top fold, consider skipping including images because of this!
Anyway, hereâs the demo:
CodePen Embed Fallback https://codepen.io/zellwk/pen/QWoQwEy Masonry Layout with CSS Grid
What in the magic is this?!
Now, there are a ton of things Iâve included in this demo, even though there are only 66 lines of JavaScript:
- You can define the masonry with any number of columns.
- Each item can span multiple columns.
- We wait for media to load before calculating the size of each item.
- We made it responsive by listening to changes with the
ResizeObserver
.
These make my implementation incredibly robust and ready for production use, while also way more flexible than many Flexbox masonry knockoffs out there on the interwebs.
Now, a hot tip
If you combine this with Tailwindâs responsive variants and arbitrary values, you can include even more flexibility into this masonry grid without writing more CSS.
Okay, before you get hyped up any further, letâs come back to the main question: How the heck does this work?
Letâs start with a polyfill
Firefox already supports masonry layouts via the second campâs syntax. Hereâs the CSS you need to create a CSS masonry grid layout in Firefox.
.masonry {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(var(--item-width, 200px), 100%), 1fr)
);
grid-template-rows: masonry;
grid-auto-flow: dense; /* Optional, but recommended */
}
Since Firefox already has native masonry support, naturally we shouldnât mess around with it. The best way to check if masonry is supported by default is to check if grid-template-rows
can hold the masonry
value.
function isMasonrySupported(container) {
return getComputedStyle(container).gridTemplateRows === 'masonry'
}
If masonry is supported, weâll skip our implementation. Otherwise, weâll do something about it.
const containers = document.querySelectorAll('.masonry')
containers.forEach(async container => {
if (isMasonrySupported(container)) return
})
Masonry layout made simple
Now, I want to preface this segment that Iâm not the one who invented this technique.
I figured out this technique when I was digging through the web, searching for possible ways to implement a masonry grid today. So kudos goes to the unknown developer who developed the idea first â and perhaps me for understanding, converting, and using it.
The technique goes like this:
- We set
grid-auto-rows
to0px
. - Then we set
row-gap
to1px
. - Then we get the itemâs height through
getBoundingClientRect
. - We then size the itemâs ârow allocationâ by adding the
height
thecolumn-gap
value together.
This is really unintuitive if youâve been using CSS Grid the standard way. But once you get this, you can also grasp how this works!
Now, because this is so unintuitive, weâre gonna take things step-by-step so you see how this whole thing evolves into the final output.
Step by step
First, we set grid-auto-rows
to 0px
. This is whacky because every grid item will effectively have âzero heightâ. Yet, at the same time, CSS Grid maintains the order of the columns and rows!
containers.forEach(async container => {
// ...
container.style.gridAutoRows = '0px'
})

Second, we set row-gap
to 1px
. Once we do this, you begin to notice an initial stacking of the rows, each one one pixel below the previous one.
containers.forEach(async container => {
// ...
container.style.gridAutoRows = '0px'
container.style.setProperty('row-gap', '1px', 'important')
})

Third, assuming there are no images or other media elements in the grid items, we can easily get the height of each grid item with getBoundingClientRect
.
We can then restore the âheightâ of the grid item in CSS Grid by substituting grow-row-end
with the height
value. This works because each row-gap
is now 1px
tall.
When we do this, you can see the grid beginning to take shape. Each item is now (kinda) back at their respective positions:
containers.forEach(async container => {
// ...
let items = container.children
layout({ items })
})
function layout({ items }) {
items.forEach(item => {
const ib = item.getBoundingClientRect()
item.style.gridRowEnd = `span ${Math.round(ib.height)}`
})
}

We now need to restore the row gap between items. Thankfully, since masonry grids usually have the same column-gap
and row-gap
values, we can grab the desired row gap by reading column-gap
values.
Once we do that, we add it to grid-row-end
to expand the number of rows (the âheightâ) taken up by the item in the grid:
containers.forEach(async container => {
// ...
const items = container.children
const colGap = parseFloat(getComputedStyle(container).columnGap)
layout({ items, colGap })
})
function layout({ items, colGap }) {
items.forEach(item => {
const ib = item.getBoundingClientRect()
item.style.gridRowEnd = `span ${Math.round(ib.height + colGap)}`
})
}

And, just like that, weâve made the masonry grid! Everything from here on is simply to make this ready for production.
Waiting for media to load
Try adding an image to any grid item and youâll notice that the grid breaks. Thatâs because the itemâs height will be âwrongâ.

Itâs wrong because we took the height
value before the image was properly loaded. The DOM doesnât know the dimensions of the image yet. To fix this, we need to wait for the media to load before running the layout
function.
We can do this with the following code (which I shall not explain since this is not much of a CSS trick đ ):
containers.forEach(async container => {
// ...
try {
await Promise.all([areImagesLoaded(container), areVideosLoaded(container)])
} catch(e) {}
// Run the layout function after images are loaded
layout({ items, colGap })
})
// Checks if images are loaded
async function areImagesLoaded(container) {
const images = Array.from(container.querySelectorAll('img'))
const promises = images.map(img => {
return new Promise((resolve, reject) => {
if (img.complete) return resolve()
img.onload = resolve
img.onerror = reject
})
})
return Promise.all(promises)
}
// Checks if videos are loaded
function areVideosLoaded(container) {
const videos = Array.from(container.querySelectorAll('video'))
const promises = videos.map(video => {
return new Promise((resolve, reject) => {
if (video.readyState === 4) return resolve()
video.onloadedmetadata = resolve
video.onerror = reject
})
})
return Promise.all(promises)
}
VoilĂ , we have a CSS masnory grid that works with images and videos!

Making it responsive
This is a simple step. We only need to use the ResizeObserver API to listen for any change in dimensions of the masonry grid container.
When thereâs a change, we run the layout
function again:
containers.forEach(async container => {
// ...
const observer = new ResizeObserver(observerFn)
observer.observe(container)
function observerFn(entries) {
for (const entry of entries) {
layout({colGap, items})
}
}
})
This demo uses the standard Resize Observer API. But you can make it simpler by using the refined resizeObserver
function we built the other day.
containers.forEach(async container => {
// ...
const observer = resizeObserver(container, {
callback () {
layout({colGap, items})
}
})
})
Thatâs pretty much it! You now have a robust masonry grid that you can use in every working browser that supports CSS Grid!
Exciting, isnât it? This implementation is so simple to use!
Masonry grid with Splendid Labz
If youâre not adverse to using code built by others, maybe you might want to consider grabbing the one Iâve built for you in Splendid Labz.
To do that, install the helper library and add the necessary code:
# Installing the library
npm install @splendidlabz/styles
/* Import all layouts code */
@import '@splendidlabz/layouts';
// Use the masonry script
import { masonry } from '@splendidlabz/styles/scripts'
masonry()
One last thing
Iâve been building a ton of tools to help make web development much easier for you and me. Iâve parked them all under the Splendid Labz brand â and one of these examples is this masonry grid I showed you today.
If you love this, you might be interested in other layout utilities that makes layout super simple to build.
Now, I hope you have enjoyed this article today. Go unleash your new CSS masonry grid if you wish to, and all the best!