
How to Create a Masonry Layout with CSS
A masonry layout arranges items of different heights into columns, packing each new item into whichever column is currently shortest. There are no fixed rows, so there are no awkward gaps under short items. The result looks like a brick wall laid on its side, which is where the name comes from. Pinterest made the pattern famous, and today you'll see it on portfolios, recipe sites, mood boards, testimonial walls, and image-heavy blogs.
For years, a true masonry layout required a JavaScript library like Masonry.js to measure every item and position it absolutely. CSS has been closing that gap. In this guide, we'll look at the pure CSS techniques that work in every browser today, their trade-offs, and the native masonry support that's arriving through grid-lanes, with a progressive enhancement pattern so you can adopt it safely.
What Makes Masonry Hard
A regular CSS Grid has both columns and rows. Every item in a row shares the same row track, so if one card in a row is tall, the others in that row get empty space below them. That's great for tables and card grids with equal heights, but not for content with naturally varying heights.
Masonry needs a layout with columns but no rows, where each item is placed in the shortest column so far. There are two important properties to keep in mind as we compare techniques:
- Packing: are items tightly packed with no gaps?
- Order: do items read left to right, row by row, in the order they appear in the HTML? Readers usually expect the newest or most important items at the top, across the first "row".
Each technique gets some of these right.
Technique 1: CSS Multi-Column Layout
The simplest pure CSS masonry uses multi-column layout, which has been supported everywhere for years:
<div class="masonry-columns">
<article class="tile">
<img
src="/images/lemon-tart.jpg"
width="600"
height="800"
alt="Lemon tart with meringue peaks"
/>
<h3>Lemon meringue tart</h3>
</article>
<article class="tile">
<h3>Quick weeknight pasta</h3>
<p>Twenty minutes, one pan, and pantry staples.</p>
</article>
<!-- more tiles -->
</div>
.masonry-columns {
columns: 16rem;
column-gap: 1rem;
}
.tile {
break-inside: avoid;
margin-bottom: 1rem;
padding: 1rem;
border-radius: 0.75rem;
background: #ffffff;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
}
.tile img {
display: block;
width: 100%;
height: auto;
border-radius: 0.5rem;
}
columns: 16remcreates as many columns as fit, each at least 16rem wide. It's responsive with no media queries.break-inside: avoidstops a tile from being split across two columns, which would otherwise happen by default.margin-bottomcreates the vertical gap. Multi-column layout doesn't supportrow-gap.
The result is tightly packed and looks exactly like masonry. The catch is order. Multi-column flows content top to bottom, then left to right, like a newspaper. Item 2 appears below item 1, not beside it. If you have 30 items in three columns, the first "row" shows items 1, 11, and 21.
That's fine for content with no inherent order, like a gallery of photos or a wall of testimonials. It's a problem for chronological content like a blog feed, where the newest posts should be across the top. It also makes adding items dynamically awkward, because new items reflow every column.
One more subtle issue: a margin at the top of a column can leave uneven alignment. If your tiles have margins on both sides, use only margin-bottom, as above, or put the spacing in padding on a wrapper.
Technique 2: Flexbox Columns
If you control the markup, you can split items into column containers yourself:
<div class="masonry-flex">
<div class="masonry-flex__col">
<article class="tile">Item 1</article>
<article class="tile">Item 4</article>
</div>
<div class="masonry-flex__col">
<article class="tile">Item 2</article>
<article class="tile">Item 5</article>
</div>
<div class="masonry-flex__col">
<article class="tile">Item 3</article>
<article class="tile">Item 6</article>
</div>
</div>
.masonry-flex {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.masonry-flex__col {
display: flex;
flex-direction: column;
flex: 1;
gap: 1rem;
min-width: 0;
}
By distributing items round-robin (1, 2, 3 in the first positions, then 4, 5, 6), the first visual row reads in the expected order. This is how many React masonry components work under the hood: they compute the column assignment in JavaScript or on the server.
The downsides: the number of columns is fixed in the markup, so responsive changes mean re-rendering; round-robin assignment doesn't guarantee the shortest column gets the next item, so columns can end up uneven; and the DOM order (1, 4, 2, 5, 3, 6) doesn't match reading order, which matters for keyboard and screen reader users.
Technique 3: Grid with Row Spans
A clever trick uses a regular Grid with very small row tracks, then makes each item span as many rows as its height requires:
.masonry-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
grid-auto-rows: 8px;
column-gap: 1rem;
}
.masonry-grid > .tile {
align-self: start;
grid-row-end: span var(--rows, 20);
}
Each item needs a --rows value equal to its height divided by the row track size. If you know the heights ahead of time, such as image tiles with known dimensions and fixed caption heights, you can compute it on the server. Otherwise a small script measures each item:
const grid = document.querySelector(".masonry-grid");
const ROW = 8;
const GAP = 16;
function layout() {
for (const item of grid.children) {
const height = item.getBoundingClientRect().height;
item.style.setProperty("--rows", Math.ceil((height + GAP) / ROW));
}
}
new ResizeObserver(layout).observe(grid);
grid
.querySelectorAll("img")
.forEach((img) => img.addEventListener("load", layout));
align-self: start keeps each item at its natural content height instead of stretching to fill its span, so the measurement is always accurate. Adding the gap to the height gives vertical spacing between items, since row-gap would add spacing between every tiny row track instead.
This approach keeps a sensible DOM order, reads roughly left to right, and packs tightly, especially with grid-auto-flow: dense. But it isn't pure CSS, and it re-runs on resize. If you're writing JavaScript anyway, it's a good middle ground.
Technique 4: Native Masonry with grid-lanes
Native masonry support has been one of the most requested CSS features for years, and after a long debate about syntax, the CSS Working Group has settled on a dedicated display type: display: grid-lanes. It reuses Grid's track sizing, so if you know Grid, you already know most of it:
.masonry {
display: grid-lanes;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: 1rem;
}
That's it. You define the lanes (the columns) with grid-template-columns, exactly like a regular grid. There are no rows. Each item goes into whichever lane gives it the highest available position, which in practice means the shortest column. Items keep their DOM order as much as possible, reading left to right across the top.
Because it builds on Grid, the usual Grid features work:
.masonry {
display: grid-lanes;
grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
gap: 1rem;
}
/* A featured tile that spans two lanes */
.masonry > .tile--featured {
grid-column: span 2;
}
/* Pin a specific item to the first lane */
.masonry > .tile--pinned {
grid-column: 1;
}
You can also lay lanes out horizontally by defining grid-template-rows instead of columns, which produces a brick-style layout of items with varying widths flowing in rows.
The spec also includes a property for controlling how strictly the "shortest lane" rule applies. When two lanes differ in height by only a few pixels, strictly picking the shortest can make items jump around unpredictably from the reader's point of view. A tolerance lets the browser treat nearly-equal lanes as tied and keep items in source order. The property name and default are among the details still being finalized, so check the current spec and browser documentation before using it.
Browser Support for grid-lanes
Be careful here. At the time of writing, display: grid-lanes is available in experimental form: Safari's Technology Preview has implemented it, and Chromium has been developing it behind a flag. Firefox had an earlier experimental implementation based on the older grid-template-rows: masonry syntax, also behind a flag. It isn't yet something you can rely on in stable browsers for all your users, so check caniuse for the current status before shipping.
You may also see older articles using display: masonry or grid-template-rows: masonry. Those were competing proposals during the syntax debate; grid-lanes is the direction browsers have agreed on.
Progressive Enhancement: Use It Today
The good news is that you can write for native masonry now and fall back gracefully. Multi-column layout makes an excellent fallback because it needs no JavaScript and works everywhere:
/* Fallback: multi-column masonry */
.masonry {
columns: 16rem;
column-gap: 1rem;
}
.masonry > .tile {
break-inside: avoid;
margin-bottom: 1rem;
}
/* Enhancement: native masonry */
@supports (display: grid-lanes) {
.masonry {
display: grid-lanes;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: 1rem;
columns: auto;
}
.masonry > .tile {
margin-bottom: 0;
}
}
Browsers without support get the newspaper-order column layout, which looks nearly identical. Browsers with support get proper left-to-right ordering. Nobody gets a broken page.
If order is important for your content and you need it everywhere today, use the Grid row-span technique as the fallback instead, and skip the script when native support is detected:
if (!CSS.supports("display", "grid-lanes")) {
// run the row-span layout() function from Technique 3
}
Accessibility and Order
Whatever technique you choose, keep reading order in mind. Screen readers and keyboard navigation follow the DOM, not the visual layout. With multi-column layout, the DOM order and the visual column order match (down each column), but the visual "rows" don't. With Flexbox columns, the DOM order is split by column. With native grid-lanes, the placement follows DOM order as closely as possible, which is one of its biggest advantages.
A few guidelines:
- For unordered collections (photo walls, testimonials, mood boards), any technique is fine.
- For chronological or ranked content, prefer native grid-lanes or the row-span technique, which keep newer items near the top.
- Make each tile a clear, self-contained unit with a heading, so users navigating by headings can move through the collection easily.
- Avoid
grid-auto-flow: densefor ordered content, since it deliberately reorders items.
Performance Tips
- Set image dimensions. Masonry layouts depend on item heights, so images without
widthandheightattributes cause items to jump as they load. With dimensions set, the browser can lay everything out before a single image arrives. - Lazy load images further down the page with
loading="lazy". - Avoid heavy JavaScript where possible. Multi-column layout and grid-lanes are laid out by the browser's layout engine, which is far faster than measuring and positioning items with script.
- Paginate or virtualize very long collections. Infinite masonry walls with thousands of items can get heavy regardless of technique.
Which Technique Should You Use?
- Order doesn't matter, zero JavaScript: multi-column layout.
- Order matters, JavaScript is acceptable: Grid with row spans.
- Server-rendered with fixed column counts: Flexbox columns.
- Future-facing:
display: grid-lanesbehind@supports, with multi-column or row spans as the fallback.
Conclusion
Masonry has been a JavaScript job for most of the web's history, but you have good CSS options today. Multi-column layout gives you a tightly packed, fully responsive masonry wall in a few lines, at the cost of top-to-bottom ordering. Flexbox columns and the Grid row-span technique fix ordering with a little help from the server or a script. And native masonry, now standardized as display: grid-lanes, brings true left-to-right masonry with the full power of Grid track sizing.
Write your layout with @supports (display: grid-lanes) today, keep a solid fallback, and your masonry wall will quietly upgrade itself as browsers ship native support.


