For more than a decade, building a Pinterest-style gallery meant loading a JavaScript library, measuring every card, and re-running the layout on every resize. That era is basically over. A CSS grid masonry layout is now achievable with pure CSS in three different ways, and each one has a very different set of trade-offs once you add responsive breakpoints and hundreds of images.
This tutorial walks through all three approaches with copy-paste code, shows exactly where each one breaks, and ends with a progressive enhancement pattern you can ship today without shipping a single kilobyte of JavaScript.
What is a masonry grid layout?
A masonry grid layout is a layout where one axis is a strict grid and the other is content-driven. In the most common version, columns have fixed, equal widths, but items keep their natural height and each new item is placed into the shortest column available. The result is a staggered, brick-like wall with no wasted vertical whitespace.
That is exactly what standard CSS Grid does not do by default. In a regular grid, rows are shared: every item in a row is as tall as the tallest item in that row, which creates the gaps masonry is supposed to eliminate. Solving that gap is the entire point of the three methods below.

The three methods at a glance
| Method | JavaScript needed | Reading order | Works with unknown heights | Browser support (Aug 2026) |
|---|---|---|---|---|
1. Grid + grid-auto-rows spans |
None, if you know the aspect ratios | Left to right, row by row | No | Universal |
2. Multi-column (columns) |
None | Top to bottom, column by column | Yes | Universal |
| 3. Native masonry / grid lanes | None | Left to right, shortest column first | Yes | Preview / flagged, not universal yet |
Method 1: CSS Grid with grid-auto-rows and row spans
This is the classic “fake masonry” that has been floating around since 2017. The idea: create a grid with a very small row height (say 8px), then make each card span the number of rows that matches its height. Because rows are tiny, cards can end at almost any vertical position, and the staggered effect appears.
The markup
<ul class="masonry">
<li class="masonry__item masonry__item--portrait">
<img src="/img/01.avif" width="800" height="1200" alt="">
</li>
<li class="masonry__item masonry__item--landscape">
<img src="/img/02.avif" width="1200" height="800" alt="">
</li>
<li class="masonry__item masonry__item--square">
<img src="/img/03.avif" width="1000" height="1000" alt="">
</li>
</ul>
The CSS
.masonry {
--row: 8px;
--gap: 16px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-auto-rows: var(--row);
column-gap: var(--gap);
row-gap: 0;
margin: 0;
padding: 0;
list-style: none;
}
.masonry__item {
display: grid; /* lets the child stretch to the reserved box */
padding-bottom: var(--gap); /* the vertical "gap", since row-gap must be 0 */
}
/* aspect ratio buckets: span = target height / row height */
.masonry__item--landscape { grid-row: span 24; } /* ~192px */
.masonry__item--square { grid-row: span 32; } /* ~256px */
.masonry__item--portrait { grid-row: span 44; } /* ~352px */
.masonry__item--tall { grid-row: span 56; } /* ~448px */
.masonry__item img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
Why row-gap must be zero
This trips up almost everyone. When you set both grid-auto-rows and row-gap, the real height of an item is span × rowHeight + (span - 1) × rowGap. The gap multiplies with the span count, so a card spanning 44 rows with a 16px row gap becomes absurdly tall. The fix is to keep row-gap: 0 and create the visual spacing with padding-bottom (or a margin on an inner wrapper).
Limitations you need to know
- You must know the height in advance. It works for image galleries where the CMS stores the aspect ratio, and it fails completely for cards containing text of unknown length.
- Images are cropped, not fitted. Because
object-fit: coverfills a box you decided on, the real aspect ratio is only approximated. Photographers hate this. - Responsive breakpoints multiply the classes. At 240px columns a portrait card needs 44 rows, but at 380px columns the same ratio needs about 70 rows. You end up rewriting every span inside every media query, or you switch to
calc()with container queries, which gets ugly fast. - Columns are not balanced. Items are placed row by row, so the last row is often ragged and one column can finish much earlier than the others. It is not true shortest-column packing.
Verdict: good for a fixed, curated gallery with known ratios. Poor for user-generated content.

Method 2: CSS multi-column, the honest fallback
Multi-column layout has done real masonry since Internet Explorer 10. It is not grid, but it is pure CSS, it handles unknown heights, and it never breaks.
.gallery-columns {
columns: 240px 4; /* min column width, max column count */
column-gap: 1rem;
}
.gallery-columns > .card {
break-inside: avoid;
-webkit-column-break-inside: avoid; /* older WebKit safety */
margin: 0 0 1rem;
display: inline-block; /* prevents stray fragment bugs */
width: 100%;
}
.gallery-columns img {
display: block;
width: 100%;
height: auto;
}
The columns: 240px 4 shorthand is doing all the responsive work for you: the browser fits as many 240px columns as the container allows, up to four. No media queries required.
Limitations
- Reading order runs down, not across. Items 1 to 5 fill column one, then 6 to 10 fill column two. If your content is chronological (a blog archive, a product feed sorted by relevance), this is visually wrong: the newest item and the fifth-newest sit side by side visually only by accident.
- No item can span columns in a useful way, so “featured” double-width cards are out.
- Column count changes reshuffle everything. Going from four to three columns at a breakpoint moves nearly every card to a new position, which looks jarring and kills any scroll-position restoration.
- Long pages get heavy. Multicol has to fragment content to balance columns. With several hundred image cards, the browser recalculates fragmentation on every resize, and it is measurably slower than grid on low-end mobile.
- Focus order can feel odd for keyboard users, since tab order follows DOM order (down the columns) while eyes usually scan across.
Verdict: the best universal fallback for unordered image walls. Avoid it when sequence matters.
Method 3: native CSS masonry (grid lanes)
This is the one everybody has been waiting for. The CSS Working Group spent years debating whether masonry should be a separate display type or an extension of Grid, and the resolution was to build it into CSS Grid. In the current specification work it appears as a lanes value on the grid track properties, documented by MDN as “grid lanes layout” and shipped in WebKit preview builds as CSS Grid Lanes. Firefox has carried the earlier masonry keyword behind a flag for years, and Chromium teams have been running public experiments and asking developers to test both syntaxes.
The syntax
.gallery-native {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-template-rows: lanes; /* the grid lanes syntax */
gap: 1rem;
}
/* older experimental keyword, still what Firefox exposes behind its flag */
@supports (grid-template-rows: masonry) {
.gallery-native { grid-template-rows: masonry; }
}
That is the whole layout. No spans, no row math, no JavaScript, no padding hacks. Items keep their natural height and each one drops into the shortest lane.
Packing control
Two behaviours are being standardised alongside it, so check MDN before relying on them in production:
- Ordered placement (the default): items are placed in DOM order into the shortest lane, which keeps reading order sensible.
- Balanced or dense packing (via
item-pack): the browser is allowed to reorder placement to minimise the ragged bottom edge. Great for pure photo walls, risky for anything where sequence carries meaning.
What still works, and what does not
Because masonry is part of Grid, you keep most of the grid toolbox:
repeat(auto-fill, minmax())andsubgrid-friendly track sizing on the grid axis- named lines, so a hero card can still do
grid-column: 1 / -1 gap, alignment properties and container queries all behave normally
What you lose on the masonry axis: there are no real rows, so anything that depends on row lines (row-based placement, row subgrid, grid-row: 2 / 4) simply does not apply on that axis.
The honest limitation in 2026
Support is not universal yet. As of August 2026 you should treat native masonry as a progressive enhancement: available in preview or flagged builds, moving fast, but not something to ship as the only layout for a production gallery. The good news is that the fallback story is unusually clean, as shown next.
The production pattern: layered progressive enhancement
Write the multi-column version as your baseline, then upgrade to native masonry with @supports. If the browser does not understand the value, the whole block is ignored and users still get a perfectly good masonry wall. Masonry? In CSS tackles the same question from another angle.
/* 1. Baseline: multi-column masonry, works in every browser */
.gallery {
columns: 240px 4;
column-gap: 1rem;
}
.gallery > .card {
break-inside: avoid;
display: inline-block;
width: 100%;
margin: 0 0 1rem;
}
/* 2. Enhancement: real CSS grid masonry where the engine supports it */
@supports (grid-template-rows: lanes) or (grid-template-rows: masonry) {
.gallery {
columns: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1rem;
}
.gallery > .card {
display: block;
margin: 0;
}
}
@supports (grid-template-rows: masonry) {
.gallery { grid-template-rows: masonry; }
}
@supports (grid-template-rows: lanes) {
.gallery { grid-template-rows: lanes; }
}
Order matters here: put the lanes block last so that when a browser supports both keywords, the standardised one wins.

Responsive breakpoints: where masonry layouts actually break
Most masonry bugs are not layout bugs, they are breakpoint bugs. A short checklist we run on every gallery build:
- Use intrinsic sizing instead of media queries.
repeat(auto-fill, minmax(240px, 1fr))andcolumns: 240px 4both adapt automatically. Hard-codedgrid-template-columns: repeat(4, 1fr)plus five media queries is five times the maintenance. - Know the difference between auto-fill and auto-fit.
auto-fillkeeps empty tracks,auto-fitcollapses them so remaining items stretch. On a gallery with only two or three items,auto-fitproduces giant stretched cards. For masonry,auto-fillis usually the safer default. - Decide what happens at one column. Below roughly 480px, masonry degenerates into a single stack, which is fine. Just make sure your minimum track width is not so large that you get a horizontal scrollbar on a 320px viewport.
- Scale the gap.
gap: clamp(0.5rem, 2vw, 1.5rem)keeps the rhythm sane from phone to 4K. - Prefer container queries for reusable components. If the same gallery appears full width and inside a sidebar, size the columns against the container, not the viewport, with
container-type: inline-size. - Test the reshuffle. Resize slowly across every breakpoint. With method 1 and method 2, items jump to entirely new positions. If your users deep-link into a gallery or use browser back, this is a real UX cost.
Image-heavy pages: performance rules that matter more than the layout
A masonry gallery is, by definition, an image-heavy page. The layout technique is rarely the bottleneck. These are:
- Always set
widthandheightattributes (or anaspect-ratioin CSS) on every image. This is what stops cumulative layout shift while images stream in, and with native masonry it lets the browser compute lane heights before the bytes arrive. - Lazy load below the fold only:
loading="lazy"on everything except the first row, andfetchpriority="high"on the largest visible image. - Match
sizesto your column width. If columns are 240px to 400px wide, serving 1600px files is wasted bandwidth. Something likesizes="(min-width: 1200px) 300px, (min-width: 600px) 45vw, 90vw"is far more accurate than100vw. - Serve AVIF with a WebP fallback through
<picture>. On a 200-image wall this is often a 60 to 70 percent transfer saving. - Add
content-visibility: autowithcontain-intrinsic-sizeon cards far down the page to skip rendering work. Be careful: on the multicol method this can interfere with fragmentation, so test it. - Paginate or use an infinite scroll with a real “load more” button. Beyond roughly 300 to 500 cards, every pure CSS method starts to feel heavy on mid-range Android devices.

Accessibility notes
- Keyboard tab order always follows DOM order, never visual order. With grid masonry that is normally fine. With
item-packdense packing, visual order and DOM order can diverge, so avoid dense packing for interactive cards. - Use a real list (
<ul>/<li>) so screen readers announce the item count. - Give decorative images an empty
alt=""and give meaningful ones real descriptions. A wall of 200 images named “image1” is a genuinely hostile experience. - Respect
prefers-reduced-motionif you animate cards in as they enter the viewport.
So which method should you use?
- Curated photo gallery, ratios known from the CMS, order does not matter: method 2 (multi-column) today, upgraded with the
@supportsblock for native masonry. - Cards with text of unpredictable length: method 2 or native masonry. Method 1 will fail.
- You need featured full-width items, named lines, or precise column control: native masonry with a grid fallback that accepts slightly uneven rows.
- You need pixel-perfect shortest-column packing in every browser right now, with animated filtering and sorting: that remains the one honest use case for a JavaScript library. Everything else can be CSS.
The direction of travel is clear: within a couple of release cycles, grid-template-rows: lanes will make the other two methods historical curiosities. Writing your CSS in layers today means you get the upgrade for free, with no rewrite.
FAQ
What is a masonry grid layout?
It is a layout with fixed, equal-width columns and content-driven, variable heights. Each item is placed into the shortest available column, producing the staggered brick pattern popularised by Pinterest. Originally covered on https://piccalil.li.
Can CSS Grid do masonry without JavaScript?
Yes, in three ways: by faking it with a tiny grid-auto-rows value plus row spans, by using CSS multi-column layout, or by using the native masonry / grid lanes value on grid-template-rows in browsers that support it. None of them require JavaScript.
Is grid-template-rows: masonry supported everywhere in 2026?
No. As of August 2026 native masonry is implemented in preview and flagged builds rather than being available across all stable browsers. Use it inside an @supports block with a multi-column fallback and check MDN or caniuse for the current status before removing the fallback.
What is the difference between CSS masonry and CSS grid lanes?
They describe the same layout behaviour. “Masonry” was the original keyword proposal, while “grid lanes” is the naming used as the feature was folded into the CSS Grid specification: one axis is a strict grid, the other stacks items into lanes. Supporting both keywords with two @supports blocks is the safe approach during the transition.
Why does my CSS grid masonry have huge gaps between items?
Almost always because you combined grid-auto-rows with a non-zero row-gap. The gap is added between every spanned row, so it multiplies with the span count. Set row-gap: 0 and create spacing with padding or margin inside the card. It is argued more carefully on webflow.com.
Is multi-column or CSS Grid better for masonry?
Multi-column handles unknown heights and needs no JavaScript, but it orders items down each column instead of across. CSS Grid keeps left-to-right reading order and lets items span columns, but needs known heights until native masonry is universally available. Choose based on whether your content order carries meaning.
Do I still need Masonry.js?
Only for advanced behaviour such as animated filtering, sorting, drag and drop reordering, or perfect shortest-column packing in legacy browsers. For a standard gallery, pure CSS is lighter, faster and does not block rendering.
Need a gallery that loads fast and looks right on every screen?
At FatCow Web Design we build image-heavy sites where the layout is only half the job: the other half is image pipelines, Core Web Vitals and a CMS your team can actually use. If your gallery is slow, shifting on load, or still dragging a layout library into every page, get in touch and we will audit it for you.
