EmpireUI
Get Pro
← Blog7 min read#css-grid#masonry-layout#css-grid-masonry

CSS Masonry Layout: Native Grid masonry Value vs JavaScript

Native CSS masonry is finally real — but should you drop your JavaScript solution today? Here's what the spec actually does, where it falls short, and how to decide.

A Pinterest-style grid of photos with varying heights arranged in a masonry column layout

Native CSS Masonry Is Real Now — Sort Of

Honestly, the CSS Working Group has been arguing about masonry layout since 2020, and the spec has shipped in browsers in a state that's... complicated. Firefox enabled grid-template-rows: masonry behind a flag years ago. Chrome shipped an experimental version in 2024 under the masonry keyword. By late 2025, support landed in Chrome 131 and Safari 18.2 behind layout.css.grid-template-masonry-value.enabled. So yes — native masonry exists. But the browser support story is still not "ship it to production without a fallback."

The core idea is elegant. You define a grid with explicit columns, then set one axis to masonry, and the browser handles the bin-packing for you. No JavaScript. No ResizeObserver loops. No layout flicker on first paint. That's genuinely appealing, especially when you look at how much code a typical Masonry.js integration requires.

Before we get into the comparison, let's be clear about what we're measuring: raw capability, developer ergonomics, performance at scale, and SSR friendliness. The "right" answer depends heavily on which of those matters most to your project right now.

How the CSS grid-template-rows: masonry Syntax Works

The syntax is a direct extension of CSS Grid. You use display: grid, define your columns normally, then set grid-template-rows: masonry (or grid-template-columns: masonry for a horizontal flow). The browser then places items into the column with the shortest remaining height, exactly like JavaScript masonry libraries have done manually for years.

Here's a minimal working example you can drop into a stylesheet today:

.masonry-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: masonry;
  gap: 16px;
  align-tracks: stretch;
}

/* Responsive: 2 cols on tablet, 1 on mobile */
@media (max-width: 768px) {
  .masonry-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 480px) {
  .masonry-grid {
    grid-template-columns: 1fr;
  }
}

Notice the align-tracks property — that's new too. It controls how tracks are aligned within the masonry axis. You've got stretch, start, end, center, and space-between as options. Most of the time you want stretch. The spec also introduces masonry-auto-flow to control whether the browser prioritizes filling the shortest column or maintains source order. Default is pack, which gives you true masonry behavior.

JavaScript Masonry Libraries: What They Actually Do

The classic approach — used by Masonry.js, Isotope, and custom React implementations — is to render all items invisibly first, measure their heights via the DOM, then absolutely position them with calculated top and left values. It works. It's been working since 2011. But the process has real costs.

First, you get a layout flash. The browser renders, JavaScript measures, JavaScript repositions. On slower connections or complex card components, users see a jump. You can hide this with opacity tricks, but it's a hack. Second, you need ResizeObserver to handle dynamic content and window resizing — more code, more potential bugs. Third, absolutely positioned grids break normal document flow, which complicates things like sticky footers and CSS grid siblings.

In React specifically, you're either reaching for a library like react-masonry-css (which uses column-based CSS, not true masonry) or you're writing a custom hook with useLayoutEffect, useRef, and a ResizeObserver. Here's what a real implementation looks like — stripped to its essentials:

import { useLayoutEffect, useRef, useState } from 'react';

interface MasonryProps {
  children: React.ReactNode[];
  columns?: number;
  gap?: number;
}

export function Masonry({ children, columns = 3, gap = 16 }: MasonryProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [columnHeights, setColumnHeights] = useState<number[]>(
    new Array(columns).fill(0)
  );

  useLayoutEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const items = Array.from(container.children) as HTMLElement[];
    const colWidth = (container.offsetWidth - gap * (columns - 1)) / columns;
    const heights = new Array(columns).fill(0);

    items.forEach((item) => {
      const shortestCol = heights.indexOf(Math.min(...heights));
      item.style.position = 'absolute';
      item.style.width = `${colWidth}px`;
      item.style.left = `${shortestCol * (colWidth + gap)}px`;
      item.style.top = `${heights[shortestCol]}px`;
      heights[shortestCol] += item.offsetHeight + gap;
    });

    container.style.height = `${Math.max(...heights)}px`;
    setColumnHeights(heights);
  }, [children, columns, gap]);

  return (
    <div
      ref={containerRef}
      style={{ position: 'relative' }}
    >
      {children}
    </div>
  );
}

That's roughly 40 lines for a basic implementation, and it doesn't handle images loading late, dynamic content changes, or nested grids. Adding those features triples the code. That's the tax you pay for compatibility.

Performance at Scale: Native CSS Wins Clearly

Run a gallery with 200 cards on a mid-range Android device. With JavaScript masonry, you're looking at a layout task that fires on mount, again after every image loads, again on every resize. Chrome DevTools will show you layout thrashing — forced synchronous layouts triggered by reading offsetHeight mid-script. On a Pixel 4a, a naive implementation can block the main thread for 80–120ms per resize event.

Native CSS masonry pushes all of that into the browser's layout engine. It runs in C++, not JavaScript. It can batch across frames. It doesn't trigger layout thrashing because there's no JavaScript reading layout values mid-paint. The browser's internal bin-packing runs at the same time as the rest of the layout pass.

Where does JavaScript still win on performance? Image loading coordination. If your cards contain images and you want the masonry to reflow after each image loads, native CSS handles this automatically — the grid re-calculates as content dimensions change. But if you're using blur-up placeholders with fixed aspect ratios (common in Next.js Image), you've already pre-computed the heights and the JavaScript approach can actually be more predictable. This is genuinely context-dependent.

The SSR Problem With JavaScript Masonry

Here's the thing: JavaScript masonry is fundamentally a client-side operation. You can't measure DOM heights on the server. This means in a Next.js or Remix app, your masonry grid will always render in an unpositioned state on the server, then jump into position after hydration. Google's crawler sees the unpositioned state first. If you care about SEO for a gallery page, that's a real problem.

Native CSS masonry sidesteps this entirely. The CSS is in the stylesheet, the browser handles layout — whether that browser is a real user's Chrome or a headless Chrome crawling your Next.js page. Your SSR output is correct on first render. No hydration mismatch. No layout shift.

For components built with glassmorphism styling or other visual effects where the layout underpins the aesthetics, that layout stability matters a lot. A glass card that snaps into position 200ms after page load breaks the illusion entirely. Native masonry, when supported, gives you that stability for free.

Browser Support Strategy: Progressive Enhancement

As of late 2026, grid-template-rows: masonry has support in Chrome 131+, Safari 18.2+, and Firefox 130+ (no flag needed). Edge follows Chrome. That's a solid majority of users — roughly 88-90% depending on your analytics. But that 10-12% matters if you're building something commercial.

The cleanest approach is progressive enhancement using @supports. Fallback to a CSS columns layout (which approximates masonry visually, though items flow top-to-bottom within each column rather than into the shortest column):

/* Fallback: CSS columns approximation */
.photo-grid {
  columns: 3;
  column-gap: 16px;
}

.photo-grid > * {
  break-inside: avoid;
  margin-bottom: 16px;
}

/* Native masonry for supporting browsers */
@supports (grid-template-rows: masonry) {
  .photo-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: masonry;
    gap: 16px;
    columns: unset;
    column-gap: unset;
  }

  .photo-grid > * {
    break-inside: unset;
    margin-bottom: unset;
  }
}

This gives you native masonry where available, CSS columns elsewhere, and zero JavaScript for the layout. If your content is images or cards without interactive reordering, this approach works for most gallery use cases. If you need filtering, sorting, or drag-to-reorder, you're back in JavaScript territory regardless of CSS masonry support.

Pairing this with Tailwind CSS requires a little custom config since there's no first-party Tailwind utility for masonry yet. In Tailwind v4.0.2, you'd add it via @layer utilities or the addUtilities plugin API. The CSS is still yours to write; Tailwind just handles the rest of the design system.

When to Stick With JavaScript Masonry in React

Don't abandon your JavaScript solution just because native CSS exists. There are specific scenarios where JS masonry is still the right call. First: interactive filtering with animation. Libraries like Isotope combine masonry layout with FLIP animations when items filter in/out. CSS masonry has no equivalent animation primitive — you can't animate items rearranging across the grid. If that interaction is core to your product (think an agency portfolio with category filters), JavaScript wins.

Second: server-side height knowledge. If you're pre-computing card heights server-side (maybe your cards are fixed-height per type, or you're storing dimensions in a database), you can server-render a perfectly positioned absolute-layout masonry grid with zero client-side layout work. That's actually faster than CSS masonry in this narrow case because the browser skips the bin-packing calculation entirely.

Third: virtualized lists. For galleries with thousands of items, you're using a virtualizer like react-virtual anyway. CSS Grid masonry can't virtualize — it needs to know the full grid to calculate column assignments. JavaScript masonry implementations can be wired to a virtualizer because item positions are computed values you control.

For projects using advanced visual techniques like CSS Houdini paint worklets or canvas animations as card backgrounds, the layout mechanism is almost secondary — pick whichever causes fewer conflicts with your paint pipeline. In practice, CSS masonry plays nicer because it doesn't fight with absolute positioning.

The Honest Verdict for Your Next Project

So which should you use? For a new project targeting modern browsers (internal tool, SaaS dashboard, developer-facing product), start with native CSS masonry plus a CSS columns fallback. You'll write less code, get better performance, avoid hydration mismatches, and your layout will work without JavaScript loading at all. That's a significant reliability win.

For a project that needs IE11 support (I know, I know — some enterprise contracts still require it), interactive filtering animations, or thousands of virtualized items, a JavaScript solution is still the right call. react-masonry-css for simple cases, a custom hook with ResizeObserver for more control, Isotope if you need animation primitives.

What about the near future? The CSS spec is still evolving. masonry-auto-flow: next (placing items in source order rather than shortest-column) is in the spec and partially implemented. align-tracks: space-evenly is coming. The property that lets you span items across multiple columns in a masonry grid — spanning two columns while masonry flows other items around it — is still a work in progress. The foundation is solid. The details are still settling.

FAQ

Can I use CSS masonry in production today without a polyfill?

If your analytics show Chrome 131+, Safari 18.2+, and Firefox 130+ covering your user base (check caniuse.com for current numbers), you can ship native masonry with a CSS columns fallback via @supports. No polyfill needed. For apps where layout breaking for 10-15% of users is unacceptable, add a JavaScript fallback for older browsers only — not the full library, just the positioning logic.

Does grid-template-rows: masonry work with CSS Grid subgrid?

Not yet as of late 2026. Subgrid and masonry are separate CSS Grid extensions and combining them in a single element is not specified. You can use masonry on a parent grid and subgrid on child elements for different purposes, but a single element can't declare both masonry and subgrid axes simultaneously.

Why does my native CSS masonry layout look different from my JavaScript masonry layout?

The default masonry-auto-flow is 'pack', which places each new item into the shortest column at that moment. Some JavaScript libraries use 'ordered' placement (left-to-right, filling columns sequentially) or 'balanced' algorithms. Check your JS library's placement algorithm and set masonry-auto-flow: next on the CSS grid to force source-order placement if visual consistency matters more than optimal packing.

How do I get 4 columns on desktop, 2 on tablet, 1 on mobile with native CSS masonry?

Use standard @media queries with repeat() and 1fr columns. The masonry value stays the same across breakpoints — only the column definition changes. Or use repeat(auto-fill, minmax(280px, 1fr)) with grid-template-rows: masonry for a fully fluid grid that adjusts column count based on available space without any media queries.

Does Next.js Image component work with CSS masonry?

Yes, but watch the aspect ratio. Next.js Image with fill layout requires a positioned parent with explicit dimensions, which can conflict with masonry's variable-height flow. Use Next.js Image with explicit width and height props (or sizes with responsive layout) so the image has intrinsic dimensions the masonry algorithm can use for height calculation.

What gap value should I use for a card gallery masonry layout?

16px (1rem) is a common baseline for dense galleries. 24px looks better for card grids with padding. The gap property in CSS masonry works identically to regular CSS Grid — it applies to both row and column gaps unless you use row-gap and column-gap separately. Don't use margin on individual cards; the gap property is handled by the layout engine and performs better.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

CSS Subgrid: Real Layout Problems It Solves That Grid Can'tCSS Subgrid in Production: Aligning Children Across Rows and ColsAdvanced Tailwind Grid: Template Areas, Subgrid, Auto PlacementRTL Design System Support: Right-to-Left Layout in React