12 CSS Masonry Layouts12 / 12
CSS Grid Template Rows Masonry Fallback Chrome Firefox
The spec-watch demo: one wall of numbered tiles, three layout engines layered by @supports — CSS columns as the universal base, grid-template-rows: masonry where the Firefox-lineage syntax runs, display: masonry where the newer proposal runs. A live status panel tells you which engine YOUR browser chose (pure CSS feature detection — the badges flip inside the same @supports blocks), and the numbered tiles make the item-order difference between engines visible instead of theoretical.
Published
The code
<section class="msn-12" aria-label="Native CSS masonry with fallback demo">
<div class="msn-12__stage" tabindex="0">
<header class="msn-12__head">
<div><h2>Engine check</h2><p>One wall · three layout engines · zero broken browsers</p></div>
</header>
<div class="msn-12__panel" role="status">
<div class="msn-12__badge msn-12__badge--cols"><strong>CSS columns</strong><span></span></div>
<div class="msn-12__badge msn-12__badge--gtr"><strong>grid-template-rows: masonry</strong><span></span></div>
<div class="msn-12__badge msn-12__badge--disp"><strong>display: masonry</strong><span></span></div>
</div>
<div class="msn-12__wall">
<article class="msn-12__tile" style="--h:1;--a:oklch(0.7 0.12 300);--b:oklch(0.48 0.14 330)"><b>01</b><h3>Watch the numbers</h3><p>Columns snake them down each lane; native masonry marches them left-to-right into the shortest track.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.75 0.1 200);--b:oklch(0.5 0.12 230)"><b>02</b><h3>Base layer</h3><p>Multicol serves everyone.</p></article>
<article class="msn-12__tile" style="--h:2;--a:oklch(0.78 0.1 85);--b:oklch(0.55 0.13 60)"><b>03</b><h3>First upgrade</h3><p>grid-template-rows: masonry — the Firefox-lineage syntax, in Nightly behind layout.css.grid-template-masonry-value.enabled. Order-preserving packing, real grid tracks, subgrid-friendly.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.72 0.11 150);--b:oklch(0.48 0.12 170)"><b>04</b><h3>Second upgrade</h3><p>display: masonry, per the newer proposal.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.68 0.12 20);--b:oklch(0.45 0.13 0)"><b>05</b><h3>The reset rule</h3><p>When grid wins, margin-bottom must yield to gap — or Nightly double-spaces.</p></article>
<article class="msn-12__tile" style="--h:2;--a:oklch(0.65 0.1 260);--b:oklch(0.42 0.11 280)"><b>06</b><h3>Why bother?</h3><p>Native masonry is the only engine that packs unknown heights AND keeps reading order — the thing demos 01 and 03 each give up half of. That's worth a three-line bet on the future.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.8 0.08 330);--b:oklch(0.58 0.11 350)"><b>07</b><h3>Flags flip off</h3><p>Never ship the bet without the base.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.74 0.09 110);--b:oklch(0.5 0.11 130)"><b>08</b><h3>@supports tests parsing</h3><p>Accepted syntax behind a disabled flag can style anyway — the cascade survives it.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.7 0.1 240);--b:oklch(0.46 0.11 260)"><b>09</b><h3>Test it today</h3><p>Firefox Nightly or Chrome Canary.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.76 0.11 45);--b:oklch(0.52 0.13 25)"><b>10</b><h3>Cleanup path</h3><p>When support crosses your floor, deleting the fallback is a six-line diff.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.66 0.12 310);--b:oklch(0.44 0.13 290)"><b>11</b><h3>Shared core only</h3><p>Tracks + gap; skip proposal-specific extras.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.72 0.1 180);--b:oklch(0.48 0.11 200)"><b>12</b><h3>The endgame</h3><p>One wall, one syntax, no JavaScript. We're one WG resolution away.</p></article>
</div>
</div>
</section><section class="msn-12" aria-label="Native CSS masonry with fallback demo">
<div class="msn-12__stage" tabindex="0">
<header class="msn-12__head">
<div><h2>Engine check</h2><p>One wall · three layout engines · zero broken browsers</p></div>
</header>
<div class="msn-12__panel" role="status">
<div class="msn-12__badge msn-12__badge--cols"><strong>CSS columns</strong><span></span></div>
<div class="msn-12__badge msn-12__badge--gtr"><strong>grid-template-rows: masonry</strong><span></span></div>
<div class="msn-12__badge msn-12__badge--disp"><strong>display: masonry</strong><span></span></div>
</div>
<div class="msn-12__wall">
<article class="msn-12__tile" style="--h:1;--a:oklch(0.7 0.12 300);--b:oklch(0.48 0.14 330)"><b>01</b><h3>Watch the numbers</h3><p>Columns snake them down each lane; native masonry marches them left-to-right into the shortest track.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.75 0.1 200);--b:oklch(0.5 0.12 230)"><b>02</b><h3>Base layer</h3><p>Multicol serves everyone.</p></article>
<article class="msn-12__tile" style="--h:2;--a:oklch(0.78 0.1 85);--b:oklch(0.55 0.13 60)"><b>03</b><h3>First upgrade</h3><p>grid-template-rows: masonry — the Firefox-lineage syntax, in Nightly behind layout.css.grid-template-masonry-value.enabled. Order-preserving packing, real grid tracks, subgrid-friendly.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.72 0.11 150);--b:oklch(0.48 0.12 170)"><b>04</b><h3>Second upgrade</h3><p>display: masonry, per the newer proposal.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.68 0.12 20);--b:oklch(0.45 0.13 0)"><b>05</b><h3>The reset rule</h3><p>When grid wins, margin-bottom must yield to gap — or Nightly double-spaces.</p></article>
<article class="msn-12__tile" style="--h:2;--a:oklch(0.65 0.1 260);--b:oklch(0.42 0.11 280)"><b>06</b><h3>Why bother?</h3><p>Native masonry is the only engine that packs unknown heights AND keeps reading order — the thing demos 01 and 03 each give up half of. That's worth a three-line bet on the future.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.8 0.08 330);--b:oklch(0.58 0.11 350)"><b>07</b><h3>Flags flip off</h3><p>Never ship the bet without the base.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.74 0.09 110);--b:oklch(0.5 0.11 130)"><b>08</b><h3>@supports tests parsing</h3><p>Accepted syntax behind a disabled flag can style anyway — the cascade survives it.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.7 0.1 240);--b:oklch(0.46 0.11 260)"><b>09</b><h3>Test it today</h3><p>Firefox Nightly or Chrome Canary.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.76 0.11 45);--b:oklch(0.52 0.13 25)"><b>10</b><h3>Cleanup path</h3><p>When support crosses your floor, deleting the fallback is a six-line diff.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.66 0.12 310);--b:oklch(0.44 0.13 290)"><b>11</b><h3>Shared core only</h3><p>Tracks + gap; skip proposal-specific extras.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.72 0.1 180);--b:oklch(0.48 0.11 200)"><b>12</b><h3>The endgame</h3><p>One wall, one syntax, no JavaScript. We're one WG resolution away.</p></article>
</div>
</div>
</section>.msn-12,
.msn-12 *,
.msn-12 *::before,
.msn-12 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.msn-12 {
--bg: oklch(0.975 0.005 260);
--card: oklch(0.995 0.002 260);
--ink: oklch(0.23 0.015 270);
--mut: oklch(0.53 0.015 270);
--line: oklch(0.89 0.008 260);
--acc: oklch(0.55 0.19 265);
--on: oklch(0.55 0.15 155);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
display: block;
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: var(--bg);
}
.msn-12__stage {
width: 100%;
container: msn12/inline-size;
height: 100vh;
height: 100svh;
overflow-y: auto;
background: var(--bg);
}
.msn-12__head {
padding: 20px clamp(16px,3cqi,28px) 0;
}
.msn-12__head h2 {
font-size: clamp(18px,2.6cqi,24px);
letter-spacing: -.02em;
}
.msn-12__head p {
font-size: 12.5px;
color: var(--mut);
margin-top: 3px;
}
.msn-12__panel {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 14px clamp(16px,3cqi,28px);
}
.msn-12__badge {
display: grid;
gap: 2px;
padding: 10px 14px;
border-radius: 11px;
border: 1px solid var(--line);
background: var(--card);
}
.msn-12__badge strong {
font-size: 12px;
font-family: ui-monospace,Menlo,Consolas,monospace;
letter-spacing: -.01em;
}
.msn-12__badge span {
font-size: 11px;
font-weight: 700;
color: var(--mut);
}
.msn-12__badge--cols span::after {
content: "active — serving this browser";
color: var(--on);
}
.msn-12__badge--gtr span::after {
content: "not supported here";
}
.msn-12__badge--disp span::after {
content: "not supported here";
}
.msn-12__wall {
columns: 220px;
column-gap: 12px;
padding: 4px clamp(16px,3cqi,28px) clamp(18px,3cqi,30px);
}
.msn-12__tile {
break-inside: avoid;
margin-bottom: 12px;
border-radius: 14px;
border: 1px solid var(--line);
background: var(--card);
padding: 16px;
display: grid;
gap: 7px;
position: relative;
overflow: clip;
}
.msn-12__tile::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 4px;
background: linear-gradient(90deg,var(--a),var(--b));
}
.msn-12__tile b {
font-size: 12px;
font-weight: 800;
font-family: ui-monospace,Menlo,Consolas,monospace;
color: var(--acc);
}
.msn-12__tile h3 {
font-size: 14.5px;
letter-spacing: -.01em;
}
.msn-12__tile p {
font-size: 12.5px;
line-height: 1.55;
color: var(--mut);
text-wrap: pretty;
}
.msn-12__tile[style*="--h:1"] p {
min-height: 3.2em;
}
.msn-12__tile[style*="--h:2"] p {
min-height: 6.4em;
}
@supports (grid-template-rows: masonry) {
.msn-12__wall {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(min(220px,100%),1fr));
grid-template-rows: masonry;
gap: 12px;
}
.msn-12__tile {
margin-bottom: 0;
}
.msn-12__badge--gtr span::after {
content: "ACTIVE — packing this wall";
color: var(--on);
}
.msn-12__badge--cols span::after {
content: "standing by (fallback)";
color: var(--mut);
}
}
@supports (display: masonry) {
.msn-12__wall {
display: masonry;
}
.msn-12__badge--disp span::after {
content: "ACTIVE — packing this wall";
color: var(--on);
}
.msn-12__badge--gtr span::after {
content: "standing by";
color: var(--mut);
}
.msn-12__badge--cols span::after {
content: "standing by (fallback)";
color: var(--mut);
}
}
@container msn12 (width < 480px) {
.msn-12__wall {
columns: 1;
}
.msn-12__panel {
gap: 6px;
}
}
@media (prefers-reduced-motion: reduce) {
.msn-12 * {
transition-duration: .01ms !important;
}
}.msn-12,
.msn-12 *,
.msn-12 *::before,
.msn-12 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.msn-12 {
--bg: oklch(0.975 0.005 260);
--card: oklch(0.995 0.002 260);
--ink: oklch(0.23 0.015 270);
--mut: oklch(0.53 0.015 270);
--line: oklch(0.89 0.008 260);
--acc: oklch(0.55 0.19 265);
--on: oklch(0.55 0.15 155);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
display: block;
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: var(--bg);
}
.msn-12__stage {
width: 100%;
container: msn12/inline-size;
height: 100vh;
height: 100svh;
overflow-y: auto;
background: var(--bg);
}
.msn-12__head {
padding: 20px clamp(16px,3cqi,28px) 0;
}
.msn-12__head h2 {
font-size: clamp(18px,2.6cqi,24px);
letter-spacing: -.02em;
}
.msn-12__head p {
font-size: 12.5px;
color: var(--mut);
margin-top: 3px;
}
.msn-12__panel {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 14px clamp(16px,3cqi,28px);
}
.msn-12__badge {
display: grid;
gap: 2px;
padding: 10px 14px;
border-radius: 11px;
border: 1px solid var(--line);
background: var(--card);
}
.msn-12__badge strong {
font-size: 12px;
font-family: ui-monospace,Menlo,Consolas,monospace;
letter-spacing: -.01em;
}
.msn-12__badge span {
font-size: 11px;
font-weight: 700;
color: var(--mut);
}
.msn-12__badge--cols span::after {
content: "active — serving this browser";
color: var(--on);
}
.msn-12__badge--gtr span::after {
content: "not supported here";
}
.msn-12__badge--disp span::after {
content: "not supported here";
}
.msn-12__wall {
columns: 220px;
column-gap: 12px;
padding: 4px clamp(16px,3cqi,28px) clamp(18px,3cqi,30px);
}
.msn-12__tile {
break-inside: avoid;
margin-bottom: 12px;
border-radius: 14px;
border: 1px solid var(--line);
background: var(--card);
padding: 16px;
display: grid;
gap: 7px;
position: relative;
overflow: clip;
}
.msn-12__tile::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 4px;
background: linear-gradient(90deg,var(--a),var(--b));
}
.msn-12__tile b {
font-size: 12px;
font-weight: 800;
font-family: ui-monospace,Menlo,Consolas,monospace;
color: var(--acc);
}
.msn-12__tile h3 {
font-size: 14.5px;
letter-spacing: -.01em;
}
.msn-12__tile p {
font-size: 12.5px;
line-height: 1.55;
color: var(--mut);
text-wrap: pretty;
}
.msn-12__tile[style*="--h:1"] p {
min-height: 3.2em;
}
.msn-12__tile[style*="--h:2"] p {
min-height: 6.4em;
}
@supports (grid-template-rows: masonry) {
.msn-12__wall {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(min(220px,100%),1fr));
grid-template-rows: masonry;
gap: 12px;
}
.msn-12__tile {
margin-bottom: 0;
}
.msn-12__badge--gtr span::after {
content: "ACTIVE — packing this wall";
color: var(--on);
}
.msn-12__badge--cols span::after {
content: "standing by (fallback)";
color: var(--mut);
}
}
@supports (display: masonry) {
.msn-12__wall {
display: masonry;
}
.msn-12__badge--disp span::after {
content: "ACTIVE — packing this wall";
color: var(--on);
}
.msn-12__badge--gtr span::after {
content: "standing by";
color: var(--mut);
}
.msn-12__badge--cols span::after {
content: "standing by (fallback)";
color: var(--mut);
}
}
@container msn12 (width < 480px) {
.msn-12__wall {
columns: 1;
}
.msn-12__panel {
gap: 6px;
}
}
@media (prefers-reduced-motion: reduce) {
.msn-12 * {
transition-duration: .01ms !important;
}
}Here's a working CSS Masonry Layout from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Grid Template Rows Masonry Fallback Chrome Firefox
Source: https://codefronts.com/layouts/css-masonry-layouts/css-grid-template-rows-masonry-fallback-chrome-firefox/
The spec-watch demo: one wall of numbered tiles, three layout engines layered by @supports — CSS columns as the universal base, grid-template-rows: masonry where the Firefox-lineage syntax runs, display: masonry where the newer proposal runs. A live status panel tells you which engine YOUR browser chose (pure CSS feature detection — the badges flip inside the same @supports blocks), and the numbered tiles make the item-order difference between engines visible instead of theoretical.
## HTML
```html
<section class="msn-12" aria-label="Native CSS masonry with fallback demo">
<div class="msn-12__stage" tabindex="0">
<header class="msn-12__head">
<div><h2>Engine check</h2><p>One wall · three layout engines · zero broken browsers</p></div>
</header>
<div class="msn-12__panel" role="status">
<div class="msn-12__badge msn-12__badge--cols"><strong>CSS columns</strong><span></span></div>
<div class="msn-12__badge msn-12__badge--gtr"><strong>grid-template-rows: masonry</strong><span></span></div>
<div class="msn-12__badge msn-12__badge--disp"><strong>display: masonry</strong><span></span></div>
</div>
<div class="msn-12__wall">
<article class="msn-12__tile" style="--h:1;--a:oklch(0.7 0.12 300);--b:oklch(0.48 0.14 330)"><b>01</b><h3>Watch the numbers</h3><p>Columns snake them down each lane; native masonry marches them left-to-right into the shortest track.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.75 0.1 200);--b:oklch(0.5 0.12 230)"><b>02</b><h3>Base layer</h3><p>Multicol serves everyone.</p></article>
<article class="msn-12__tile" style="--h:2;--a:oklch(0.78 0.1 85);--b:oklch(0.55 0.13 60)"><b>03</b><h3>First upgrade</h3><p>grid-template-rows: masonry — the Firefox-lineage syntax, in Nightly behind layout.css.grid-template-masonry-value.enabled. Order-preserving packing, real grid tracks, subgrid-friendly.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.72 0.11 150);--b:oklch(0.48 0.12 170)"><b>04</b><h3>Second upgrade</h3><p>display: masonry, per the newer proposal.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.68 0.12 20);--b:oklch(0.45 0.13 0)"><b>05</b><h3>The reset rule</h3><p>When grid wins, margin-bottom must yield to gap — or Nightly double-spaces.</p></article>
<article class="msn-12__tile" style="--h:2;--a:oklch(0.65 0.1 260);--b:oklch(0.42 0.11 280)"><b>06</b><h3>Why bother?</h3><p>Native masonry is the only engine that packs unknown heights AND keeps reading order — the thing demos 01 and 03 each give up half of. That's worth a three-line bet on the future.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.8 0.08 330);--b:oklch(0.58 0.11 350)"><b>07</b><h3>Flags flip off</h3><p>Never ship the bet without the base.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.74 0.09 110);--b:oklch(0.5 0.11 130)"><b>08</b><h3>@supports tests parsing</h3><p>Accepted syntax behind a disabled flag can style anyway — the cascade survives it.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.7 0.1 240);--b:oklch(0.46 0.11 260)"><b>09</b><h3>Test it today</h3><p>Firefox Nightly or Chrome Canary.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.76 0.11 45);--b:oklch(0.52 0.13 25)"><b>10</b><h3>Cleanup path</h3><p>When support crosses your floor, deleting the fallback is a six-line diff.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.66 0.12 310);--b:oklch(0.44 0.13 290)"><b>11</b><h3>Shared core only</h3><p>Tracks + gap; skip proposal-specific extras.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.72 0.1 180);--b:oklch(0.48 0.11 200)"><b>12</b><h3>The endgame</h3><p>One wall, one syntax, no JavaScript. We're one WG resolution away.</p></article>
</div>
</div>
</section>
```
## CSS
```css
.msn-12,
.msn-12 *,
.msn-12 *::before,
.msn-12 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.msn-12 {
--bg: oklch(0.975 0.005 260);
--card: oklch(0.995 0.002 260);
--ink: oklch(0.23 0.015 270);
--mut: oklch(0.53 0.015 270);
--line: oklch(0.89 0.008 260);
--acc: oklch(0.55 0.19 265);
--on: oklch(0.55 0.15 155);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
display: block;
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: var(--bg);
}
.msn-12__stage {
width: 100%;
container: msn12/inline-size;
height: 100vh;
height: 100svh;
overflow-y: auto;
background: var(--bg);
}
.msn-12__head {
padding: 20px clamp(16px,3cqi,28px) 0;
}
.msn-12__head h2 {
font-size: clamp(18px,2.6cqi,24px);
letter-spacing: -.02em;
}
.msn-12__head p {
font-size: 12.5px;
color: var(--mut);
margin-top: 3px;
}
.msn-12__panel {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 14px clamp(16px,3cqi,28px);
}
.msn-12__badge {
display: grid;
gap: 2px;
padding: 10px 14px;
border-radius: 11px;
border: 1px solid var(--line);
background: var(--card);
}
.msn-12__badge strong {
font-size: 12px;
font-family: ui-monospace,Menlo,Consolas,monospace;
letter-spacing: -.01em;
}
.msn-12__badge span {
font-size: 11px;
font-weight: 700;
color: var(--mut);
}
.msn-12__badge--cols span::after {
content: "active — serving this browser";
color: var(--on);
}
.msn-12__badge--gtr span::after {
content: "not supported here";
}
.msn-12__badge--disp span::after {
content: "not supported here";
}
.msn-12__wall {
columns: 220px;
column-gap: 12px;
padding: 4px clamp(16px,3cqi,28px) clamp(18px,3cqi,30px);
}
.msn-12__tile {
break-inside: avoid;
margin-bottom: 12px;
border-radius: 14px;
border: 1px solid var(--line);
background: var(--card);
padding: 16px;
display: grid;
gap: 7px;
position: relative;
overflow: clip;
}
.msn-12__tile::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 4px;
background: linear-gradient(90deg,var(--a),var(--b));
}
.msn-12__tile b {
font-size: 12px;
font-weight: 800;
font-family: ui-monospace,Menlo,Consolas,monospace;
color: var(--acc);
}
.msn-12__tile h3 {
font-size: 14.5px;
letter-spacing: -.01em;
}
.msn-12__tile p {
font-size: 12.5px;
line-height: 1.55;
color: var(--mut);
text-wrap: pretty;
}
.msn-12__tile[style*="--h:1"] p {
min-height: 3.2em;
}
.msn-12__tile[style*="--h:2"] p {
min-height: 6.4em;
}
@supports (grid-template-rows: masonry) {
.msn-12__wall {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(min(220px,100%),1fr));
grid-template-rows: masonry;
gap: 12px;
}
.msn-12__tile {
margin-bottom: 0;
}
.msn-12__badge--gtr span::after {
content: "ACTIVE — packing this wall";
color: var(--on);
}
.msn-12__badge--cols span::after {
content: "standing by (fallback)";
color: var(--mut);
}
}
@supports (display: masonry) {
.msn-12__wall {
display: masonry;
}
.msn-12__badge--disp span::after {
content: "ACTIVE — packing this wall";
color: var(--on);
}
.msn-12__badge--gtr span::after {
content: "standing by";
color: var(--mut);
}
.msn-12__badge--cols span::after {
content: "standing by (fallback)";
color: var(--mut);
}
}
@container msn12 (width < 480px) {
.msn-12__wall {
columns: 1;
}
.msn-12__panel {
gap: 6px;
}
}
@media (prefers-reduced-motion: reduce) {
.msn-12 * {
transition-duration: .01ms !important;
}
}
```Here's a working CSS Masonry Layout from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Grid Template Rows Masonry Fallback Chrome Firefox
Source: https://codefronts.com/layouts/css-masonry-layouts/css-grid-template-rows-masonry-fallback-chrome-firefox/
The spec-watch demo: one wall of numbered tiles, three layout engines layered by @supports — CSS columns as the universal base, grid-template-rows: masonry where the Firefox-lineage syntax runs, display: masonry where the newer proposal runs. A live status panel tells you which engine YOUR browser chose (pure CSS feature detection — the badges flip inside the same @supports blocks), and the numbered tiles make the item-order difference between engines visible instead of theoretical.
## HTML
```html
<section class="msn-12" aria-label="Native CSS masonry with fallback demo">
<div class="msn-12__stage" tabindex="0">
<header class="msn-12__head">
<div><h2>Engine check</h2><p>One wall · three layout engines · zero broken browsers</p></div>
</header>
<div class="msn-12__panel" role="status">
<div class="msn-12__badge msn-12__badge--cols"><strong>CSS columns</strong><span></span></div>
<div class="msn-12__badge msn-12__badge--gtr"><strong>grid-template-rows: masonry</strong><span></span></div>
<div class="msn-12__badge msn-12__badge--disp"><strong>display: masonry</strong><span></span></div>
</div>
<div class="msn-12__wall">
<article class="msn-12__tile" style="--h:1;--a:oklch(0.7 0.12 300);--b:oklch(0.48 0.14 330)"><b>01</b><h3>Watch the numbers</h3><p>Columns snake them down each lane; native masonry marches them left-to-right into the shortest track.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.75 0.1 200);--b:oklch(0.5 0.12 230)"><b>02</b><h3>Base layer</h3><p>Multicol serves everyone.</p></article>
<article class="msn-12__tile" style="--h:2;--a:oklch(0.78 0.1 85);--b:oklch(0.55 0.13 60)"><b>03</b><h3>First upgrade</h3><p>grid-template-rows: masonry — the Firefox-lineage syntax, in Nightly behind layout.css.grid-template-masonry-value.enabled. Order-preserving packing, real grid tracks, subgrid-friendly.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.72 0.11 150);--b:oklch(0.48 0.12 170)"><b>04</b><h3>Second upgrade</h3><p>display: masonry, per the newer proposal.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.68 0.12 20);--b:oklch(0.45 0.13 0)"><b>05</b><h3>The reset rule</h3><p>When grid wins, margin-bottom must yield to gap — or Nightly double-spaces.</p></article>
<article class="msn-12__tile" style="--h:2;--a:oklch(0.65 0.1 260);--b:oklch(0.42 0.11 280)"><b>06</b><h3>Why bother?</h3><p>Native masonry is the only engine that packs unknown heights AND keeps reading order — the thing demos 01 and 03 each give up half of. That's worth a three-line bet on the future.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.8 0.08 330);--b:oklch(0.58 0.11 350)"><b>07</b><h3>Flags flip off</h3><p>Never ship the bet without the base.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.74 0.09 110);--b:oklch(0.5 0.11 130)"><b>08</b><h3>@supports tests parsing</h3><p>Accepted syntax behind a disabled flag can style anyway — the cascade survives it.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.7 0.1 240);--b:oklch(0.46 0.11 260)"><b>09</b><h3>Test it today</h3><p>Firefox Nightly or Chrome Canary.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.76 0.11 45);--b:oklch(0.52 0.13 25)"><b>10</b><h3>Cleanup path</h3><p>When support crosses your floor, deleting the fallback is a six-line diff.</p></article>
<article class="msn-12__tile" style="--h:0;--a:oklch(0.66 0.12 310);--b:oklch(0.44 0.13 290)"><b>11</b><h3>Shared core only</h3><p>Tracks + gap; skip proposal-specific extras.</p></article>
<article class="msn-12__tile" style="--h:1;--a:oklch(0.72 0.1 180);--b:oklch(0.48 0.11 200)"><b>12</b><h3>The endgame</h3><p>One wall, one syntax, no JavaScript. We're one WG resolution away.</p></article>
</div>
</div>
</section>
```
## CSS
```css
.msn-12,
.msn-12 *,
.msn-12 *::before,
.msn-12 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.msn-12 {
--bg: oklch(0.975 0.005 260);
--card: oklch(0.995 0.002 260);
--ink: oklch(0.23 0.015 270);
--mut: oklch(0.53 0.015 270);
--line: oklch(0.89 0.008 260);
--acc: oklch(0.55 0.19 265);
--on: oklch(0.55 0.15 155);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--ink);
container-type: inline-size;
display: block;
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: var(--bg);
}
.msn-12__stage {
width: 100%;
container: msn12/inline-size;
height: 100vh;
height: 100svh;
overflow-y: auto;
background: var(--bg);
}
.msn-12__head {
padding: 20px clamp(16px,3cqi,28px) 0;
}
.msn-12__head h2 {
font-size: clamp(18px,2.6cqi,24px);
letter-spacing: -.02em;
}
.msn-12__head p {
font-size: 12.5px;
color: var(--mut);
margin-top: 3px;
}
.msn-12__panel {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 14px clamp(16px,3cqi,28px);
}
.msn-12__badge {
display: grid;
gap: 2px;
padding: 10px 14px;
border-radius: 11px;
border: 1px solid var(--line);
background: var(--card);
}
.msn-12__badge strong {
font-size: 12px;
font-family: ui-monospace,Menlo,Consolas,monospace;
letter-spacing: -.01em;
}
.msn-12__badge span {
font-size: 11px;
font-weight: 700;
color: var(--mut);
}
.msn-12__badge--cols span::after {
content: "active — serving this browser";
color: var(--on);
}
.msn-12__badge--gtr span::after {
content: "not supported here";
}
.msn-12__badge--disp span::after {
content: "not supported here";
}
.msn-12__wall {
columns: 220px;
column-gap: 12px;
padding: 4px clamp(16px,3cqi,28px) clamp(18px,3cqi,30px);
}
.msn-12__tile {
break-inside: avoid;
margin-bottom: 12px;
border-radius: 14px;
border: 1px solid var(--line);
background: var(--card);
padding: 16px;
display: grid;
gap: 7px;
position: relative;
overflow: clip;
}
.msn-12__tile::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 4px;
background: linear-gradient(90deg,var(--a),var(--b));
}
.msn-12__tile b {
font-size: 12px;
font-weight: 800;
font-family: ui-monospace,Menlo,Consolas,monospace;
color: var(--acc);
}
.msn-12__tile h3 {
font-size: 14.5px;
letter-spacing: -.01em;
}
.msn-12__tile p {
font-size: 12.5px;
line-height: 1.55;
color: var(--mut);
text-wrap: pretty;
}
.msn-12__tile[style*="--h:1"] p {
min-height: 3.2em;
}
.msn-12__tile[style*="--h:2"] p {
min-height: 6.4em;
}
@supports (grid-template-rows: masonry) {
.msn-12__wall {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(min(220px,100%),1fr));
grid-template-rows: masonry;
gap: 12px;
}
.msn-12__tile {
margin-bottom: 0;
}
.msn-12__badge--gtr span::after {
content: "ACTIVE — packing this wall";
color: var(--on);
}
.msn-12__badge--cols span::after {
content: "standing by (fallback)";
color: var(--mut);
}
}
@supports (display: masonry) {
.msn-12__wall {
display: masonry;
}
.msn-12__badge--disp span::after {
content: "ACTIVE — packing this wall";
color: var(--on);
}
.msn-12__badge--gtr span::after {
content: "standing by";
color: var(--mut);
}
.msn-12__badge--cols span::after {
content: "standing by (fallback)";
color: var(--mut);
}
}
@container msn12 (width < 480px) {
.msn-12__wall {
columns: 1;
}
.msn-12__panel {
gap: 6px;
}
}
@media (prefers-reduced-motion: reduce) {
.msn-12 * {
transition-duration: .01ms !important;
}
}
```How this works
Native masonry finally does what neither columns nor spans can — true packing WITH reading order — but in mid-2026 it still sits behind flags while the CSSWG settles two competing syntaxes. Ship it only like this:
.wall{ columns: 220px; column-gap: 12px; } /* base: everyone */
@supports (grid-template-rows: masonry){ /* Firefox lineage */
.wall{ display:grid; columns:auto;
grid-template-columns: repeat(auto-fill, minmax(min(220px,100%),1fr));
grid-template-rows: masonry; gap:12px; }
.tile{ margin-bottom:0; }
}
@supports (display: masonry){ /* newer proposal */
.wall{ display:masonry; }
}Read the cascade bottom-up: a browser that knows display:masonry uses it; else one that knows the grid syntax uses that; else multicol — never a broken wall. The reset lines matter: when grid takes over, the multicol margin-bottom rhythm must yield to gap, or you double-space. That is the real craft of progressive enhancement — undoing the fallback's assumptions, not just adding the shiny thing.
The status panel is the same trick users can steal: each badge's ::after says 'fallback active', and the SAME @supports blocks that flip the layout also rewrite the badge text — so the detection can never lie or drift from the layout it describes. The tiles are numbered 01–12 because engines disagree about WHERE items go: columns snakes 01-05 down lane one; native masonry keeps 01,02,03 marching left-to-right, each dropping into the shortest track. Watch the numbers, not the gaps, when you test in Firefox (layout.css.grid-template-masonry-value.enabled) or Chrome Canary's Experimental Web Platform features.
Make it yours
- Track your real support floor: paste this wall into your analytics page and log which badge is active via getComputedStyle(wall).display — 'masonry', 'grid' or 'block' tells you the engine without user-agent sniffing.
- The tile set is deliberately mixed-height text cards; swap in demo 01's image pins unchanged — the three-engine cascade doesn't care what the tiles are.
- When your support data says native has crossed ~95%, delete the base block and the reset lines — the demo's structure makes the eventual cleanup a 6-line diff.
- Prefer order? Until native lands for you, demo 03's span lattice is the order-preserving stopgap; this demo's cascade is for when heights are truly unknowable.
Gotchas — read before shipping
- Do not ship native masonry WITHOUT the base layer — flags get flipped off, enterprise browsers lag years, and grid-template-rows:masonry may yet be renamed as the spec debate resolves. The cascade is the product; the native line is a bet.
- @supports tests PARSING, not correctness — a browser can accept the syntax behind a disabled flag… but the inverse never happens, so the cascade stays safe: worst case you get the fallback.
- Remember to neutralize fallback spacing inside each @supports block (margin-bottom:0 here) — 'double gap in Firefox Nightly' is this pattern's signature bug report.
- Item tolerance, track spanning and order tweaking differ between the two proposals; keep production usage to the shared core (plain tracks, gap) until the WG picks a winner.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 117+ | 17.5+ | 121+ | 117+ |
Floor set by oklch(), @container, subgrid, text-wrap as this demo is written. The core technique itself goes back further — Chrome Canary, flag.
As of mid-2026 native masonry remains behind flags in all three engines while the CSSWG weighs grid-integrated vs display:masonry syntax. The columns base serves 100% of users today — that is the point of this demo.