How to Create a True Masonry with Next.js
In an earlier tutorial, we showed you how to create a masonry layout using Tailwind CSS. However, as mentioned in that article, the result was a “fake masonry” layout because CSS alone can only arrange items vertically within columns.
In facts, at the time of writing, creating a true masonry layout purely with CSS isn’t possible. Fortunately, by using JavaScript, we can bridge this gap.
In this guide, we’ll show you how to achieve a masonry layout in Next.js. There are several ways to do this, but we’ll focus on a simple and effective approach that we’ve successfully used in two of our Tailwind CSS templates: Open PRO and Simple.
To accomplish this, we built a custom React hook called useMasonry.ts
, which includes full TypeScript support. This hook can be used within any Next.js client component, making it flexible and reusable.
Here’s the code:
import { useEffect, useState, useRef } from "react";
const useMasonry = () => {
const masonryContainer = useRef<HTMLDivElement | null>(null);
const [items, setItems] = useState<ChildNode[]>([]);
useEffect(() => {
if (masonryContainer.current) {
const masonryItem = Array.from(masonryContainer.current.children);
setItems(masonryItem);
}
}, []);
useEffect(() => {
const handleMasonry = () => {
if (!items || items.length < 1) return;
let gapSize = 0;
if (masonryContainer.current) {
gapSize = parseInt(
window
.getComputedStyle(masonryContainer.current)
.getPropertyValue("grid-row-gap"),
);
}
items.forEach((el) => {
if (!(el instanceof HTMLElement)) return;
let previous = el.previousSibling;
while (previous) {
if (previous.nodeType === 1) {
el.style.marginTop = "0";
if (
previous instanceof HTMLElement &&
elementLeft(previous) === elementLeft(el)
) {
el.style.marginTop =
-(elementTop(el) - elementBottom(previous) - gapSize) + "px";
break;
}
}
previous = previous.previousSibling;
}
});
};
handleMasonry();
window.addEventListener("resize", handleMasonry);
return () => {
window.removeEventListener("resize", handleMasonry);
};
}, [items]);
const elementLeft = (el: HTMLElement) => {
return el.getBoundingClientRect().left;
};
const elementTop = (el: HTMLElement) => {
return el.getBoundingClientRect().top + window.scrollY;
};
const elementBottom = (el: HTMLElement) => {
return el.getBoundingClientRect().bottom + window.scrollY;
};
return masonryContainer;
};
export default useMasonry;
This hook works by identifying the container element that holds the individual grid items, which should be styled using CSS grid. Once it has access to the necessary information — like the gap between items and their positions — it adjusts the vertical positioning of each item to create a true masonry effect.
You can integrate this hook into any grid element as follows:
"use client"
import useMasonry from "@/components/utils/useMasonry";
export default function MasonryPage() {
const masonryContainer = useMasonry();
return (
<div
ref={masonryContainer}
className="grid items-start gap-4 sm:grid-cols-3 md:gap-6"
>
{/* Your masonry items here */}
</div>
)
}
This approach allows you to create dynamic, responsive masonry layouts within your Next.js projects, enhancing your designs while maintaining the simplicity of using Tailwind CSS.