Navitve CSS Masonry in 2026


I’ve been using macy.js to power the photo grid on my photography section for a while. I’m grateful for that little library, but it’s gotten a bit long in the tooth. A commit hasn’t been made to it’s repo in 7 years (though admittedly it doesn’t need one).

While making some adjacent CSS changes I broke the masonry layout that relies on macy.js, and while fixing it I thought: it’s 2026, why can’t browsers do this by default by now? Turns out they can. CSS natively supports masonry layouts. It’s just behind a flag.

The state of native CSS masonry

Designers and developers have wanted browser-native masonry for nearly a decade. Pinterest of course popularized it even though web designers had been using the effect for a while. Rachel Andrew flagged it to the CSS Working Group at least as early 2017. The first real movement came around 2020, when Firefox shipped grid-template-rows: masonry behind a flag, which then triggered years of spec debate across the browsers. The keyword choice alone apparently took years to settle. Chrome and Edge finally added it in version 140 (mid-2025), and the syntax has since landed on display: grid-lanes as part of CSS Grid Level 3. (The MDN page was updated March 9, 2026, which tells you how actively this is still moving).

As of now it remains behind flags in all major browsers. But that doesn’t mean we can’t use it with a simple JavaScript polyfill.

How grid-lanes works

CSS Grid has always known how to make columns. What it couldn’t do natively is pack items into the shortest column — it assumes rows have uniform height, so shorter photos leave blank space where the next uniform row begins. display: grid-lanes fixes this directly. You define your columns, and the browser handles vertical placement automatically:

css

#photos {
  display: grid-lanes;
  grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
  row-gap: 4px;
  column-gap: 4px;
}

That’s it. No JavaScript, no span math, no layout events to manage.

Using JavaScript as a polyfill

Since grid-lanes isn’t in stable browsers yet, a small script can stand in. The approach: set grid-auto-rows: 1px to turn the grid into a pixel-precise coordinate system, then measure each item’s actual height and set its grid-row-end span explicitly.

The 1px row unit matters most when your gaps are tight. With a coarse value like grid-auto-rows: 10px, rounding errors can introduce up to 9px of phantom space below items — invisible at generous gaps, obvious at 4px. At 1px resolution the span count is exactly the pixel height of the item plus your gap, with no accumulation.

css

/* Base: clean responsive grid, no JS required */
#photos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
  grid-auto-rows: 1px;
  grid-auto-flow: dense;
  column-gap: 4px;
  row-gap: 0;
}

/* Native upgrade for supporting browsers */
@supports (display: grid-lanes) {
  #photos {
    display: grid-lanes;
    grid-auto-rows: auto;
    row-gap: 4px;
  }
}

javascript

function resizeGridItems() {
  const grid = document.getElementById('photos');
  const items = grid.querySelectorAll('.photo-item');
  const gap = 4;

  items.forEach(item => {
    const figure = item.querySelector('figure');
    if (figure) {
      const height = figure.getBoundingClientRect().height;
      item.style.gridRowEnd = `span ${Math.ceil(height + gap)}`;
    }
  });
}

Wire this to ResizeObserver and image load events. The @supports block means the script never runs in browsers that support grid-lanes natively.

Three browser states, all handled

  • No JS: clean uniform grid from the base CSS. Not masonry, but not broken.
  • JS, no grid-lanes: the polyfill gives you true masonry with 1px precision.
  • grid-lanes supported: @supports takes over, JS never runs.

When grid-lanes makes it into stable browsers releases the migration is one deletion: remove the script tag. The CSS is already written for it.