· Updated on

How to Create a True Masonry with Next.js

Preview of the masonry layout we're going to build.

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.