12 CSS Masonry Layouts01 / 12

CSS onlyMIT licensed

Pinterest Style Image Gallery Pure CSS

The layout that made masonry famous, rebuilt in two declarations and zero scripts: columns: 230px mints as many columns as fit the container, break-inside: avoid keeps every pin whole, and the browser does the packing at compositor speed. Every pin reserves its exact height with aspect-ratio (CLS score: 0 by construction), the Save action surfaces on hover AND keyboard focus, and a container query tightens the gutters wherever the wall is pasted.

Published

Live Demo
Try it

The code

<section class="msn-01" aria-label="Pinterest style pure CSS masonry gallery demo">
  <div class="msn-01__stage" tabindex="0">
    <header class="msn-01__head">
      <div><h2>Saved ideas</h2><p>184 pins · updated today</p></div>
      <nav class="msn-01__chips" aria-label="Filter pins">
        <a href="#all" aria-current="true">All</a><a href="#interiors">Interiors</a><a href="#travel">Travel</a><a href="#food">Food</a><a href="#style">Style</a>
      </nav>
    </header>
    <div class="msn-01__wall">
      <article class="msn-01__pin"><figure style="--ar:3/4;--img:url('https://images.unsplash.com/photo-1506905925346-21bda4d32df4?q=80&w=800&auto=format&fit=crop');--a:oklch(0.75 0.09 250);--b:oklch(0.45 0.12 270)"><figcaption>Alpine mornings</figcaption><a class="msn-01__save" href="#save-1">Save</a></figure><h3><a class="msn-01__link" href="#pin-1">Dolomites cabin week</a></h3><p>1.2k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:4/5;--img:url('https://images.unsplash.com/photo-1493809842364-78817add7ffb?q=80&w=800&auto=format&fit=crop');--a:oklch(0.85 0.06 80);--b:oklch(0.6 0.1 60)"><figcaption>Warm minimalism</figcaption><a class="msn-01__save" href="#save-2">Save</a></figure><h3><a class="msn-01__link" href="#pin-2">Living room in oak + linen</a></h3><p>867 saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:1/1;--img:url('https://images.unsplash.com/photo-1504674900247-0877df9cc836?q=80&w=800&auto=format&fit=crop');--a:oklch(0.7 0.13 40);--b:oklch(0.5 0.15 25)"><figcaption>Sunday table</figcaption><a class="msn-01__save" href="#save-3">Save</a></figure><h3><a class="msn-01__link" href="#pin-3">Herbed citrus roast</a></h3><p>2.4k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:16/10;--img:url('https://images.unsplash.com/photo-1507525428034-b723cf961d3e?q=80&w=800&auto=format&fit=crop');--a:oklch(0.85 0.07 220);--b:oklch(0.65 0.1 240)"><figcaption>Shoulder season</figcaption><a class="msn-01__save" href="#save-4">Save</a></figure><h3><a class="msn-01__link" href="#pin-4">Quiet beaches, October</a></h3><p>3.1k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:2/3;--img:url('https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?q=80&w=800&auto=format&fit=crop');--a:oklch(0.8 0.05 340);--b:oklch(0.55 0.1 320)"><figcaption>Capsule fits</figcaption><a class="msn-01__save" href="#save-5">Save</a></figure><h3><a class="msn-01__link" href="#pin-5">Neutral layering, autumn</a></h3><p>412 saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:4/5;--img:url('https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?q=80&w=800&auto=format&fit=crop');--a:oklch(0.65 0.08 60);--b:oklch(0.4 0.09 50)"><figcaption>Slow brew</figcaption><a class="msn-01__save" href="#save-6">Save</a></figure><h3><a class="msn-01__link" href="#pin-6">Pour-over ritual at home</a></h3><p>958 saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:3/4;--img:url('https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=800&auto=format&fit=crop');--a:oklch(0.7 0.11 150);--b:oklch(0.45 0.12 160)"><figcaption>Forest bathing</figcaption><a class="msn-01__save" href="#save-7">Save</a></figure><h3><a class="msn-01__link" href="#pin-7">Old-growth trails, PNW</a></h3><p>1.8k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:1/1;--img:url('https://images.unsplash.com/photo-1523275335684-37898b6baf30?q=80&w=800&auto=format&fit=crop');--a:oklch(0.8 0.04 100);--b:oklch(0.6 0.06 80)"><figcaption>Everyday carry</figcaption><a class="msn-01__save" href="#save-8">Save</a></figure><h3><a class="msn-01__link" href="#pin-8">Field watch, sand strap</a></h3><p>623 saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:2/3;--img:url('https://images.unsplash.com/photo-1483985988355-763728e1935b?q=80&w=800&auto=format&fit=crop');--a:oklch(0.82 0.06 20);--b:oklch(0.6 0.1 0)"><figcaption>Market day</figcaption><a class="msn-01__save" href="#save-9">Save</a></figure><h3><a class="msn-01__link" href="#pin-9">Vintage shopping, Le Marais</a></h3><p>2.9k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:16/10;--img:url('https://images.unsplash.com/photo-1449824913935-59a10b8d2000?q=80&w=800&auto=format&fit=crop');--a:oklch(0.72 0.08 260);--b:oklch(0.5 0.1 280)"><figcaption>Golden hour grid</figcaption><a class="msn-01__save" href="#save-10">Save</a></figure><h3><a class="msn-01__link" href="#pin-10">Downtown from the rooftop</a></h3><p>1.1k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:1/1;--img:url('https://images.unsplash.com/photo-1540189549336-e6e99c3679fe?q=80&w=800&auto=format&fit=crop');--a:oklch(0.78 0.1 130);--b:oklch(0.55 0.12 110)"><figcaption>Bowl food</figcaption><a class="msn-01__save" href="#save-11">Save</a></figure><h3><a class="msn-01__link" href="#pin-11">Fifteen-minute grain bowls</a></h3><p>745 saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:3/4;--img:url('https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=800&auto=format&fit=crop');--a:oklch(0.7 0.06 200);--b:oklch(0.45 0.08 220)"><figcaption>Fog lines</figcaption><a class="msn-01__save" href="#save-12">Save</a></figure><h3><a class="msn-01__link" href="#pin-12">Ridge walks before 7am</a></h3><p>534 saves</p></article>
    </div>
  </div>
</section>
.msn-01,
.msn-01 *,
.msn-01 *::before,
.msn-01 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.msn-01 {
  --gap: 14px;
  --bg: oklch(0.985 0.004 85);
  --ink: oklch(0.22 0.015 60);
  --mut: oklch(0.55 0.02 60);
  --line: oklch(0.91 0.008 85);
  --acc: oklch(0.55 0.21 25);
  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-01__stage {
  width: 100%;
  container: msn01/inline-size;
  height: 100vh;
  height: 100svh;
  overflow-y: auto;
  background: var(--bg);
}

.msn-01__head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 18px;
  padding: 16px clamp(16px,3cqi,28px);
  background: color-mix(in oklch,var(--bg) 84%,transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.msn-01__head h2 {
  font-size: clamp(18px,2.6cqi,24px);
  letter-spacing: -.02em;
}

.msn-01__head p {
  font-size: 12px;
  color: var(--mut);
  margin-top: 2px;
}

.msn-01__chips {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.msn-01__chips a {
  display: grid;
  place-items: center;
  min-height: 36px;
  padding: 0 14px;
  border-radius: 99px;
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  color: var(--ink);
  border: 1px solid var(--line);
  transition: background .2s,border-color .2s;
}

.msn-01__chips a:hover,
.msn-01__chips a:focus-visible {
  background: oklch(0.94 0.008 85);
  border-color: oklch(0.84 0.01 85);
}

.msn-01__chips a[aria-current] {
  background: var(--ink);
  border-color: var(--ink);
  color: oklch(0.99 0 0);
}

.msn-01__chips a:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.msn-01__wall {
  columns: 230px;
  column-gap: var(--gap);
  padding: clamp(16px,3cqi,28px);
}

.msn-01__pin {
  break-inside: avoid;
  margin-bottom: var(--gap);
  position: relative;
}

.msn-01__pin figure {
  position: relative;
  aspect-ratio: var(--ar,3/4);
  border-radius: 16px;
  overflow: clip;
  background: linear-gradient(145deg,var(--a),var(--b));
}

.msn-01__pin figure::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--img,none);
  background-size: cover;
  background-position: center;
  transition: scale .45s cubic-bezier(.2,.7,.2,1);
}

.msn-01__pin figure::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,oklch(0.2 0.02 60/.32),transparent 34%,transparent 66%,oklch(0.2 0.02 60/.42));
  opacity: 0;
  transition: opacity .3s;
}

.msn-01__pin:hover figure::before,
.msn-01__pin:focus-within figure::before {
  scale: 1.045;
}

.msn-01__pin:hover figure::after,
.msn-01__pin:focus-within figure::after {
  opacity: 1;
}

.msn-01__pin figcaption {
  position: absolute;
  left: 12px;
  bottom: 10px;
  z-index: 1;
  font-size: 12px;
  font-weight: 600;
  color: oklch(0.99 0 0);
  text-shadow: 0 1px 8px oklch(0.15 0.02 60/.6);
  opacity: 0;
  translate: 0 6px;
  transition: opacity .3s,translate .3s;
}

.msn-01__pin:hover figcaption,
.msn-01__pin:focus-within figcaption {
  opacity: 1;
  translate: 0 0;
}

.msn-01__save {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  display: grid;
  place-items: center;
  min-height: 38px;
  padding: 0 15px;
  border-radius: 99px;
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
  color: oklch(0.99 0 0);
  background: var(--acc);
  opacity: 0;
  translate: 0 -4px;
  transition: opacity .25s,translate .25s,background .2s;
}

.msn-01__pin:hover .msn-01__save,
.msn-01__pin:focus-within .msn-01__save {
  opacity: 1;
  translate: 0 0;
}

.msn-01__save:hover {
  background: oklch(0.48 0.21 25);
}

.msn-01__save:focus-visible {
  outline: 2px solid oklch(0.99 0 0);
  outline-offset: 2px;
}

.msn-01__pin h3 {
  margin-top: 9px;
  font-size: 14px;
  line-height: 1.35;
  letter-spacing: -.01em;
}

.msn-01__link {
  color: inherit;
  text-decoration: none;
}

.msn-01__link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 16px;
}

.msn-01__link:focus-visible {
  outline: none;
}

.msn-01__link:focus-visible::after {
  outline: 3px solid var(--acc);
  outline-offset: 2px;
}

.msn-01__pin>p {
  margin-top: 3px;
  font-size: 12px;
  color: var(--mut);
}

@media (hover: none) {
  .msn-01__save,
    .msn-01__pin figcaption {
    opacity: 1;
    translate: 0 0;
  }
}

@container msn01 (width < 480px) {
  .msn-01__wall {
    columns: 150px;
    --gap: 10px;
  }

  .msn-01__pin figure {
    border-radius: 12px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .msn-01 *,
    .msn-01 *::before,
    .msn-01 *::after {
    transition-duration: .01ms !important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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: Pinterest Style Image Gallery Pure CSS
Source: https://codefronts.com/layouts/css-masonry-layouts/pinterest-style-image-gallery-pure-css/

The layout that made masonry famous, rebuilt in two declarations and zero scripts: columns: 230px mints as many columns as fit the container, break-inside: avoid keeps every pin whole, and the browser does the packing at compositor speed. Every pin reserves its exact height with aspect-ratio (CLS score: 0 by construction), the Save action surfaces on hover AND keyboard focus, and a container query tightens the gutters wherever the wall is pasted.
## HTML
```html
<section class="msn-01" aria-label="Pinterest style pure CSS masonry gallery demo">
  <div class="msn-01__stage" tabindex="0">
    <header class="msn-01__head">
      <div><h2>Saved ideas</h2><p>184 pins · updated today</p></div>
      <nav class="msn-01__chips" aria-label="Filter pins">
        <a href="#all" aria-current="true">All</a><a href="#interiors">Interiors</a><a href="#travel">Travel</a><a href="#food">Food</a><a href="#style">Style</a>
      </nav>
    </header>
    <div class="msn-01__wall">
      <article class="msn-01__pin"><figure style="--ar:3/4;--img:url('https://images.unsplash.com/photo-1506905925346-21bda4d32df4?q=80&w=800&auto=format&fit=crop');--a:oklch(0.75 0.09 250);--b:oklch(0.45 0.12 270)"><figcaption>Alpine mornings</figcaption><a class="msn-01__save" href="#save-1">Save</a></figure><h3><a class="msn-01__link" href="#pin-1">Dolomites cabin week</a></h3><p>1.2k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:4/5;--img:url('https://images.unsplash.com/photo-1493809842364-78817add7ffb?q=80&w=800&auto=format&fit=crop');--a:oklch(0.85 0.06 80);--b:oklch(0.6 0.1 60)"><figcaption>Warm minimalism</figcaption><a class="msn-01__save" href="#save-2">Save</a></figure><h3><a class="msn-01__link" href="#pin-2">Living room in oak + linen</a></h3><p>867 saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:1/1;--img:url('https://images.unsplash.com/photo-1504674900247-0877df9cc836?q=80&w=800&auto=format&fit=crop');--a:oklch(0.7 0.13 40);--b:oklch(0.5 0.15 25)"><figcaption>Sunday table</figcaption><a class="msn-01__save" href="#save-3">Save</a></figure><h3><a class="msn-01__link" href="#pin-3">Herbed citrus roast</a></h3><p>2.4k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:16/10;--img:url('https://images.unsplash.com/photo-1507525428034-b723cf961d3e?q=80&w=800&auto=format&fit=crop');--a:oklch(0.85 0.07 220);--b:oklch(0.65 0.1 240)"><figcaption>Shoulder season</figcaption><a class="msn-01__save" href="#save-4">Save</a></figure><h3><a class="msn-01__link" href="#pin-4">Quiet beaches, October</a></h3><p>3.1k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:2/3;--img:url('https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?q=80&w=800&auto=format&fit=crop');--a:oklch(0.8 0.05 340);--b:oklch(0.55 0.1 320)"><figcaption>Capsule fits</figcaption><a class="msn-01__save" href="#save-5">Save</a></figure><h3><a class="msn-01__link" href="#pin-5">Neutral layering, autumn</a></h3><p>412 saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:4/5;--img:url('https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?q=80&w=800&auto=format&fit=crop');--a:oklch(0.65 0.08 60);--b:oklch(0.4 0.09 50)"><figcaption>Slow brew</figcaption><a class="msn-01__save" href="#save-6">Save</a></figure><h3><a class="msn-01__link" href="#pin-6">Pour-over ritual at home</a></h3><p>958 saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:3/4;--img:url('https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=800&auto=format&fit=crop');--a:oklch(0.7 0.11 150);--b:oklch(0.45 0.12 160)"><figcaption>Forest bathing</figcaption><a class="msn-01__save" href="#save-7">Save</a></figure><h3><a class="msn-01__link" href="#pin-7">Old-growth trails, PNW</a></h3><p>1.8k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:1/1;--img:url('https://images.unsplash.com/photo-1523275335684-37898b6baf30?q=80&w=800&auto=format&fit=crop');--a:oklch(0.8 0.04 100);--b:oklch(0.6 0.06 80)"><figcaption>Everyday carry</figcaption><a class="msn-01__save" href="#save-8">Save</a></figure><h3><a class="msn-01__link" href="#pin-8">Field watch, sand strap</a></h3><p>623 saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:2/3;--img:url('https://images.unsplash.com/photo-1483985988355-763728e1935b?q=80&w=800&auto=format&fit=crop');--a:oklch(0.82 0.06 20);--b:oklch(0.6 0.1 0)"><figcaption>Market day</figcaption><a class="msn-01__save" href="#save-9">Save</a></figure><h3><a class="msn-01__link" href="#pin-9">Vintage shopping, Le Marais</a></h3><p>2.9k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:16/10;--img:url('https://images.unsplash.com/photo-1449824913935-59a10b8d2000?q=80&w=800&auto=format&fit=crop');--a:oklch(0.72 0.08 260);--b:oklch(0.5 0.1 280)"><figcaption>Golden hour grid</figcaption><a class="msn-01__save" href="#save-10">Save</a></figure><h3><a class="msn-01__link" href="#pin-10">Downtown from the rooftop</a></h3><p>1.1k saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:1/1;--img:url('https://images.unsplash.com/photo-1540189549336-e6e99c3679fe?q=80&w=800&auto=format&fit=crop');--a:oklch(0.78 0.1 130);--b:oklch(0.55 0.12 110)"><figcaption>Bowl food</figcaption><a class="msn-01__save" href="#save-11">Save</a></figure><h3><a class="msn-01__link" href="#pin-11">Fifteen-minute grain bowls</a></h3><p>745 saves</p></article>
      <article class="msn-01__pin"><figure style="--ar:3/4;--img:url('https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=800&auto=format&fit=crop');--a:oklch(0.7 0.06 200);--b:oklch(0.45 0.08 220)"><figcaption>Fog lines</figcaption><a class="msn-01__save" href="#save-12">Save</a></figure><h3><a class="msn-01__link" href="#pin-12">Ridge walks before 7am</a></h3><p>534 saves</p></article>
    </div>
  </div>
</section>
```
## CSS
```css
.msn-01,
.msn-01 *,
.msn-01 *::before,
.msn-01 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.msn-01 {
  --gap: 14px;
  --bg: oklch(0.985 0.004 85);
  --ink: oklch(0.22 0.015 60);
  --mut: oklch(0.55 0.02 60);
  --line: oklch(0.91 0.008 85);
  --acc: oklch(0.55 0.21 25);
  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-01__stage {
  width: 100%;
  container: msn01/inline-size;
  height: 100vh;
  height: 100svh;
  overflow-y: auto;
  background: var(--bg);
}

.msn-01__head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 18px;
  padding: 16px clamp(16px,3cqi,28px);
  background: color-mix(in oklch,var(--bg) 84%,transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.msn-01__head h2 {
  font-size: clamp(18px,2.6cqi,24px);
  letter-spacing: -.02em;
}

.msn-01__head p {
  font-size: 12px;
  color: var(--mut);
  margin-top: 2px;
}

.msn-01__chips {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.msn-01__chips a {
  display: grid;
  place-items: center;
  min-height: 36px;
  padding: 0 14px;
  border-radius: 99px;
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  color: var(--ink);
  border: 1px solid var(--line);
  transition: background .2s,border-color .2s;
}

.msn-01__chips a:hover,
.msn-01__chips a:focus-visible {
  background: oklch(0.94 0.008 85);
  border-color: oklch(0.84 0.01 85);
}

.msn-01__chips a[aria-current] {
  background: var(--ink);
  border-color: var(--ink);
  color: oklch(0.99 0 0);
}

.msn-01__chips a:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.msn-01__wall {
  columns: 230px;
  column-gap: var(--gap);
  padding: clamp(16px,3cqi,28px);
}

.msn-01__pin {
  break-inside: avoid;
  margin-bottom: var(--gap);
  position: relative;
}

.msn-01__pin figure {
  position: relative;
  aspect-ratio: var(--ar,3/4);
  border-radius: 16px;
  overflow: clip;
  background: linear-gradient(145deg,var(--a),var(--b));
}

.msn-01__pin figure::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--img,none);
  background-size: cover;
  background-position: center;
  transition: scale .45s cubic-bezier(.2,.7,.2,1);
}

.msn-01__pin figure::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,oklch(0.2 0.02 60/.32),transparent 34%,transparent 66%,oklch(0.2 0.02 60/.42));
  opacity: 0;
  transition: opacity .3s;
}

.msn-01__pin:hover figure::before,
.msn-01__pin:focus-within figure::before {
  scale: 1.045;
}

.msn-01__pin:hover figure::after,
.msn-01__pin:focus-within figure::after {
  opacity: 1;
}

.msn-01__pin figcaption {
  position: absolute;
  left: 12px;
  bottom: 10px;
  z-index: 1;
  font-size: 12px;
  font-weight: 600;
  color: oklch(0.99 0 0);
  text-shadow: 0 1px 8px oklch(0.15 0.02 60/.6);
  opacity: 0;
  translate: 0 6px;
  transition: opacity .3s,translate .3s;
}

.msn-01__pin:hover figcaption,
.msn-01__pin:focus-within figcaption {
  opacity: 1;
  translate: 0 0;
}

.msn-01__save {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  display: grid;
  place-items: center;
  min-height: 38px;
  padding: 0 15px;
  border-radius: 99px;
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
  color: oklch(0.99 0 0);
  background: var(--acc);
  opacity: 0;
  translate: 0 -4px;
  transition: opacity .25s,translate .25s,background .2s;
}

.msn-01__pin:hover .msn-01__save,
.msn-01__pin:focus-within .msn-01__save {
  opacity: 1;
  translate: 0 0;
}

.msn-01__save:hover {
  background: oklch(0.48 0.21 25);
}

.msn-01__save:focus-visible {
  outline: 2px solid oklch(0.99 0 0);
  outline-offset: 2px;
}

.msn-01__pin h3 {
  margin-top: 9px;
  font-size: 14px;
  line-height: 1.35;
  letter-spacing: -.01em;
}

.msn-01__link {
  color: inherit;
  text-decoration: none;
}

.msn-01__link::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 16px;
}

.msn-01__link:focus-visible {
  outline: none;
}

.msn-01__link:focus-visible::after {
  outline: 3px solid var(--acc);
  outline-offset: 2px;
}

.msn-01__pin>p {
  margin-top: 3px;
  font-size: 12px;
  color: var(--mut);
}

@media (hover: none) {
  .msn-01__save,
    .msn-01__pin figcaption {
    opacity: 1;
    translate: 0 0;
  }
}

@container msn01 (width < 480px) {
  .msn-01__wall {
    columns: 150px;
    --gap: 10px;
  }

  .msn-01__pin figure {
    border-radius: 12px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .msn-01 *,
    .msn-01 *::before,
    .msn-01 *::after {
    transition-duration: .01ms !important;
  }
}
```

How this works

The entire packing engine is two declarations — this is the layout Masonry.js shipped 50KB of JavaScript to approximate:

.wall{ columns: 230px; column-gap: 14px; }
.pin { break-inside: avoid; margin-bottom: 14px; }

columns: 230px is the self-responsive form of multi-column: the browser derives the count (floor of container ÷ 230) and splits the leftover width evenly, so there are no breakpoints to write — 5 columns on a desktop, 2 in a blog embed, 1 on a watch, for free. break-inside: avoid is the one line everyone forgets; without it a pin gets sliced mid-image across two columns like a bad newspaper column break.

Know the trade before you ship it: multicol flows content DOWN column one, then column two — a snake order. For a browse-forever gallery that ranking-not-chronology order is exactly right (it is literally how Pinterest reads); for a strict newest-first feed it is wrong, and demo 03 shows the grid-span technique that preserves reading order.

The zero-CLS story: every figure declares its true aspect-ratio via an inline --ar, so the wall's full geometry is computed before a single image byte arrives. Photos ride as a background-image layer above an oklch gradient pair — offline, ad-blocked or 404, the gradient shows and the layout never moves. The whole pin is clickable through a stretched-link ::after, while Save stays a separate, individually focusable link layered above it.

Make it yours

  • Column width is the only knob: 230px180px for a dense 6-up mosaic, 300px for an airy editorial wall. The count always derives itself.
  • Gutters must match in both axes or the rhythm breaks — column-gap and the pin's margin-bottom both read one --gap token; change it in one place.
  • Real images: swap each figure's background for <img src alt loading='lazy'> with width:100%; height:auto; display:block and delete the --ar — the intrinsic ratio takes over, everything else holds.
  • Always-visible captions: delete the opacity:0 + translate pair on figcaption — the demo already forces them visible on touch screens via @media (hover:none).

Gotchas — read before shipping

  • Forgetting break-inside:avoid slices pins across columns — it is the single most-asked masonry Stack Overflow question, and it belongs on the pin, not the wall.
  • Use margin-bottom only for vertical rhythm. Top margins on the first pin of a column collapse out of the box and leave ragged column tops.
  • Never give the wall a fixed height: multicol switches from balancing to filling, and pins silently overflow into invisible extra columns on the right.
  • Animate the inner figure layer on hover, not the pin box — transforming the multicol child itself used to leave paint artifacts in Safari ≤ 16 and still forces wider repaints.

Browser support

ChromeSafariFirefoxEdge
111+16.4+113+111+

Multi-column layout itself is 2011-era universal. The stated floor comes from oklch(), aspect-ratio and container queries — swap those and this runs a decade back, minus the polish.

Techniques used in this demo

Search CodeFronts

Loading…