Masonry that glides
A CSS multi-column masonry board whose cards glide to their new column when the column width changes or the board is resized.
<header class="intro">
<h1>Masonry that glides</h1>
<p>
The columns are plain CSS multi-column: invisible <code>.cell</code>s reflow
<em>instantly</em> when the column width changes or you drag the board's edge.
Each card is anchored to its cell with <code>data-⚓️-fill</code> and
<code>data-⚓️-transition="glide"</code>, so it travels to its new column instead of teleporting.
Every cell reuses the name <code>--card</code>, fenced by <code>data-⚓️-scope-name</code>.
Zero JavaScript.
</p>
<form class="controls" aria-label="Board controls">
<fieldset class="segmented">
<legend>Column width</legend>
<label><input type="radio" name="col" value="s"> Narrow</label>
<label><input type="radio" name="col" value="m" checked> Medium</label>
<label><input type="radio" name="col" value="l"> Wide</label>
</fieldset>
<label class="xray-toggle"><input type="checkbox" name="xray"> X-ray</label>
</form>
</header>
<main class="board" data-⚓️-root>
<div class="columns">
<div class="cell" style="--h: 15rem; --hue: 30" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Salt flats at dusk</strong><span>Bonneville, UT</span></p>
</article>
</div>
<div class="cell" style="--h: 9rem; --hue: 320" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Neon alley</strong><span>Osaka</span></p>
</article>
</div>
<div class="cell" style="--h: 12rem; --hue: 170" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Kelp forest</strong><span>Monterey</span></p>
</article>
</div>
<div class="cell" style="--h: 8rem; --hue: 40" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Paper lanterns</strong><span>Hội An</span></p>
</article>
</div>
<div class="cell" style="--h: 14rem; --hue: 210" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Glacier melt</strong><span>Svalbard</span></p>
</article>
</div>
<div class="cell" style="--h: 10rem; --hue: 20" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Rust & rivets</strong><span>Pittsburgh</span></p>
</article>
</div>
<div class="cell" style="--h: 11rem; --hue: 350" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Night market</strong><span>Taipei</span></p>
</article>
</div>
<div class="cell" style="--h: 16rem; --hue: 240" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Fog bank</strong><span>Big Sur</span></p>
</article>
</div>
<div class="cell" style="--h: 9rem; --hue: 140" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Tea terraces</strong><span>Munnar</span></p>
</article>
</div>
<div class="cell" style="--h: 13rem; --hue: 50" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Sandstone arch</strong><span>Moab</span></p>
</article>
</div>
<div class="cell" style="--h: 10rem; --hue: 160" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Aurora</strong><span>Tromsø</span></p>
</article>
</div>
<div class="cell" style="--h: 15rem; --hue: 130" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Pine & moss</strong><span>Olympic NP</span></p>
</article>
</div>
<div class="cell" style="--h: 8rem; --hue: 260" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Tile mosaic</strong><span>Lisbon</span></p>
</article>
</div>
<div class="cell" style="--h: 12rem; --hue: 10" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Lava field</strong><span>Reykjanes</span></p>
</article>
</div>
<div class="cell" style="--h: 14rem; --hue: 110" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Bamboo grove</strong><span>Arashiyama</span></p>
</article>
</div>
<div class="cell" style="--h: 9rem; --hue: 280" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Harbour lights</strong><span>Hobart</span></p>
</article>
</div>
<div class="cell" style="--h: 11rem; --hue: 60" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Dune sea</strong><span>Namib</span></p>
</article>
</div>
<div class="cell" style="--h: 13rem; --hue: 230" data-⚓️-name="--card" data-⚓️-scope-name="--card">
<article class="card" data-⚓️-to="--card" data-⚓️-fill data-⚓️-transition="glide">
<div class="art" aria-hidden="true"></div>
<p class="caption"><strong>Rain window</strong><span>Seattle</span></p>
</article>
</div>
</div>
</main>/* Demo styling only. Every card's position AND size comes from
data-⚓️-fill on its cell; the glide comes from data-⚓️-transition. */
:root {
--col: 14rem;
color-scheme: dark;
background: oklch(16% 0.012 270);
color: oklch(90% 0.01 270);
font: 400 1rem/1.5 system-ui, sans-serif;
}
body {
margin: 0;
padding: clamp(1rem, 4vw, 3rem);
}
.intro {
max-width: 62ch;
margin-block-end: 1.5rem;
}
h1 {
margin: 0 0 0.5rem;
font-size: clamp(1.4rem, 3vw, 1.9rem);
font-weight: 650;
letter-spacing: -0.02em;
}
.intro p {
margin: 0;
color: oklch(72% 0.015 270);
text-wrap: pretty;
}
code {
font: 0.88em ui-monospace, monospace;
color: oklch(84% 0.09 190);
white-space: nowrap;
}
/* ---------- controls ---------- */
.controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem 1.25rem;
margin-block-start: 1.25rem;
}
.segmented {
display: flex;
border: 0;
padding: 3px;
margin: 0;
border-radius: 999px;
background: oklch(24% 0.015 270);
}
.segmented legend {
/* visually hidden but still labels the group */
position: absolute;
clip-path: inset(50%);
width: 1px;
height: 1px;
overflow: hidden;
}
.segmented label {
padding: 0.35rem 0.9rem;
border-radius: 999px;
font-size: 0.875rem;
cursor: pointer;
color: oklch(75% 0.015 270);
transition: background-color 0.2s, color 0.2s;
}
.segmented input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.segmented label:has(:checked) {
background: oklch(90% 0.01 270);
color: oklch(18% 0.01 270);
}
.segmented label:has(:focus-visible),
.xray-toggle:has(:focus-visible) {
outline: 2px solid oklch(80% 0.12 190);
outline-offset: 2px;
}
.xray-toggle {
display: inline-flex;
align-items: center;
gap: 0.4rem;
font-size: 0.875rem;
color: oklch(75% 0.015 270);
cursor: pointer;
}
.xray-toggle input { accent-color: oklch(80% 0.12 190); }
body:has([name="col"][value="s"]:checked) { --col: 10rem; }
body:has([name="col"][value="l"]:checked) { --col: 19rem; }
/* ---------- the board: plain multi-column, reflows instantly ---------- */
.board {
inline-size: min(100%, 72rem);
min-inline-size: 11rem;
max-inline-size: 100%;
box-sizing: border-box;
padding: 1rem 1.25rem 0.25rem 0;
border-inline-end: 2px dashed oklch(38% 0.02 270);
resize: horizontal;
overflow: hidden;
}
.columns {
columns: var(--col);
column-gap: 1rem;
}
/* Invisible structure: this is what the multi-column layout actually moves. */
.cell {
height: var(--h);
margin-block-end: 1rem;
break-inside: avoid;
}
/* ---------- the visible card (looks only) ---------- */
.card {
display: grid;
grid-template-rows: 1fr auto;
overflow: hidden;
border-radius: 1rem;
background: oklch(23% 0.02 var(--hue));
box-shadow:
0 0 0 1px oklch(40% 0.05 var(--hue) / 0.6),
0 10px 30px -12px oklch(0% 0 0 / 0.7);
transition-duration: 0.45s;
}
.art {
background:
radial-gradient(circle at 30% 25%, oklch(85% 0.12 calc(var(--hue) + 40) / 0.9), transparent 45%),
radial-gradient(circle at 80% 90%, oklch(55% 0.18 calc(var(--hue) - 30)), transparent 60%),
linear-gradient(160deg, oklch(62% 0.14 var(--hue)), oklch(28% 0.08 calc(var(--hue) + 60)));
}
.caption {
display: grid;
margin: 0;
padding: 0.6rem 0.8rem 0.7rem;
font-size: 0.85rem;
line-height: 1.3;
}
.caption strong {
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.caption span { color: oklch(70% 0.04 var(--hue)); }
/* ---------- X-ray: show the anchors (cells) under the cards ---------- */
body:has([name="xray"]:checked) {
& .cell {
outline: 1px dashed oklch(80% 0.14 190);
outline-offset: -1px;
border-radius: 1rem;
}
& .cell::before {
content: attr(data-⚓️-name) " ⚓️";
display: block;
padding: 0.3rem 0.5rem;
font: 0.7rem ui-monospace, monospace;
color: oklch(85% 0.14 190);
}
& .card {
opacity: 0.55;
}
}
@media (prefers-reduced-motion: reduce) {
.segmented label { transition: none; }
}data-⚓️-filldata-⚓️-namedata-⚓️-rootdata-⚓️-scope-namedata-⚓️-todata-⚓️-transition