Two Ways to Get Masonry Without JS
"Masonry" means items of varying heights packed into columns with no wasted vertical gaps — the Pinterest-style grid. Historically this required a JavaScript library (Masonry.js, Isotope) measuring each item's rendered height and absolutely positioning it. Two native CSS techniques now cover most masonry use cases with zero JavaScript, and they have different tradeoffs worth understanding before you pick one.
Approach 1: CSS Multi-Column Layout
column-count was designed for magazine-style text columns, but it produces a visually convincing masonry effect for card grids too, because it fills items into a column before moving to the next.
.masonry {
column-count: 3;
column-gap: 1rem;
}
.masonry-item {
break-inside: avoid;
margin-bottom: 1rem;
display: inline-block;
width: 100%;
}
<div class="masonry">
<div class="masonry-item"><img src="a.jpg" alt=""></div>
<div class="masonry-item"><img src="b.jpg" alt=""></div>
<div class="masonry-item"><img src="c.jpg" alt=""></div>
<!-- ... -->
</div>
break-inside: avoid is not optional — without it, a card's content can be split across the bottom of one column and the top of the next, which looks broken. display: inline-block combined with width: 100% is a common fix for a Chrome/Safari quirk where margin-bottom on block children inside columns gets ignored in some engine versions; inline-block sidesteps it reliably.
Responsive column count
.masonry {
column-count: 1;
column-gap: 1rem;
}
@media (min-width: 640px) {
.masonry { column-count: 2; }
}
@media (min-width: 1024px) {
.masonry { column-count: 3; }
}
Or skip the breakpoints entirely with column-width, which lets the browser compute the count based on available space:
.masonry {
column-width: 260px; /* browser fits as many 260px+ columns as space allows */
column-gap: 1rem;
}
The real limitation: source order
CSS columns fill down each column before moving right — item 1 goes to the top of column 1, item 2 below it, and only once column 1 is full does item 3 start column 2. True masonry algorithms place each new item into whichever column is currently shortest, which produces a more balanced layout and different visual ordering. For image galleries where item order doesn't carry meaning, this distinction rarely matters. For content where reading order matters (e.g., a blog card grid users scan left-to-right), the column-fill order can feel unintuitive since related items may end up far apart.
Approach 2: CSS Grid's masonry Value
Grid's masonry layout is closer to what JavaScript libraries actually do — it packs items into the shortest available track rather than pre-filling one axis.
.masonry-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-template-rows: masonry;
gap: 1rem;
}
This is genuinely simpler to write than the columns approach and produces the correct top-to-bottom, left-to-right reading order with shortest-track packing. The catch is browser support: grid-template-rows: masonry originated in Firefox and is defined in the CSS Grid Layout Level 3 draft, with other engines implementing it more recently. Check caniuse before depending on it without a fallback, since support is newer and less universal than multi-column layout.
Feature-detecting with @supports
.masonry-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1rem;
}
@supports (grid-template-rows: masonry) {
.masonry-grid {
grid-template-rows: masonry;
}
}
Browsers without masonry support fall back to a regular grid (equal-height rows, gaps between differently-sized items), which is a reasonable degradation — not broken, just less tightly packed.
Decision Point: Which Approach, and When to Reach for JS Instead
Use CSS columns when:
- You need masonry today across all current browsers without a fallback branch.
- Item order isn't semantically important (photo galleries, portfolio grids).
- You're fine with the "fill down, then across" ordering behavior.
Use CSS Grid masonry when:
- You control the audience/browser matrix tightly enough to rely on newer support, or you're comfortable with the
@supportsfallback degrading to a plain grid. - Reading order matters and you need true left-to-right, shortest-track packing.
- You want the simpler, more semantically correct grid-based mental model going forward.
Reach for a JavaScript library (Masonry.js, or a React-specific equivalent) when:
- You need items to animate/reflow when new content is added or removed dynamically (infinite scroll masonry feeds).
- You need horizontal masonry (packing into rows instead of columns) — neither CSS technique above handles that natively.
- You need pixel-perfect control over gutter and packing algorithm behavior across every supported browser simultaneously, without accepting the visual differences between the two CSS approaches.
Reading Order and Accessibility
Both CSS-only approaches can visually reorder content relative to how it appears in the DOM, and that matters for anyone using a screen reader or keyboard navigation, since assistive technology follows DOM order, not visual order. CSS columns fill top-to-bottom within a column before moving to the next, so item 4 might sit visually above item 2 depending on content height — a sighted mouse user scanning left-to-right can misread the intended sequence, and a screen reader user hears the actual DOM order, which may not match what's visually adjacent. CSS Grid's masonry value is better here: it preserves left-to-right, top-to-bottom placement that more closely matches source order, which is one more reason to prefer it once support is broad enough for your audience.
Practically: if the order of items carries real meaning (a chronological list of posts, a ranked set of results), lean toward CSS Grid masonry or make sure your CSS columns source order genuinely doesn't matter for comprehension — don't rely on order or visual placement to convey sequence that only exists in the stylesheet.
An Alternative Fake-Masonry Trick with Grid
Before native masonry had broad support, a common workaround used grid-auto-rows with a small row height plus grid-row: span N computed per item (often via a tiny inline style or CSS custom property set per item) to approximate packing:
.fake-masonry {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-auto-rows: 8px;
gap: 1rem;
}
.fake-masonry-item {
grid-row: span var(--row-span, 20);
}
This still requires computing --row-span from each item's actual rendered height, which in a zero-JS context means either a fixed aspect ratio per item or accepting imprecise packing. It's a reasonable middle ground when you want grid's left-to-right ordering behavior but can't yet rely on grid-template-rows: masonry, though it's more CSS complexity than either primary approach above for a result that's still an approximation rather than true masonry.
Handling Images Without Layout Shift
Masonry grids are usually image-heavy, and unsized images cause layout shift as they load — worse in a masonry grid because the whole column packing recalculates. Always set intrinsic dimensions:
<img src="photo.jpg" alt="Description" width="600" height="800" loading="lazy">
If your source images come in inconsistent dimensions and you want visually consistent card sizes before laying them into a masonry grid, resizing them to a common max-width up front with the Image Resizer reduces layout jank and keeps download weight proportional to actual display size rather than shipping full-resolution originals into a 260px column.
A Practical Starting Point
For most projects, start with CSS columns — it works everywhere today, requires no fallback logic, and the reading-order tradeoff is invisible for typical gallery content. Reserve CSS Grid masonry for new projects where you're comfortable with @supports-gated newer features, and only bring in a JavaScript library once you need dynamic reflow or horizontal packing that neither native technique provides.