16 CSS Portfolio Layouts03 / 16

CSS onlyMIT licensed

CSS Masonry Portfolio Layout No JavaScript

A Pinterest-style wall of mixed-height pieces with zero JavaScript and zero libraries: CSS multi-columns do the packing, break-inside:avoid keeps every card whole, and captions slide up over the art on hover or keyboard focus. Column count is fluid — columns: 3 220px means 'up to three, never narrower than 220px' — so the wall re-packs itself for any container it lands in.

Published

Live Demo
Try it

The code

<section class="cpl-03" aria-label="CSS masonry portfolio layout demo">
  <div class="cpl-03__stage">
    <header class="cpl-03__head">
      <h2>Field Notes</h2>
      <p>35mm &amp; medium format — no JavaScript, no libraries</p>
    </header>
    <div class="cpl-03__wall">
      <a class="cpl-03__piece" href="#p1" style="--img:url('https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=900&auto=format&fit=crop');--r:3/4;--a:oklch(0.8 0.08 250);--b:oklch(0.45 0.1 270)"><figure></figure><figcaption>Fog over Hayes<span>SF · 2026</span></figcaption></a>
      <a class="cpl-03__piece" href="#p2" style="--img:url('https://images.unsplash.com/photo-1472214103451-9374bd1c798e?q=80&w=900&auto=format&fit=crop');--r:1/1;--a:oklch(0.85 0.09 90);--b:oklch(0.6 0.13 60)"><figure></figure><figcaption>Amber hour<span>Lisbon · 2025</span></figcaption></a>
      <a class="cpl-03__piece" href="#p3" style="--img:url('https://images.unsplash.com/photo-1505118380757-91f5f5632de0?q=80&w=900&auto=format&fit=crop');--r:4/5;--a:oklch(0.78 0.1 160);--b:oklch(0.5 0.11 190)"><figure></figure><figcaption>Sea wall study<span>Cornwall · 2025</span></figcaption></a>
      <a class="cpl-03__piece" href="#p4" style="--img:url('https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?q=80&w=900&auto=format&fit=crop');--r:3/2;--a:oklch(0.82 0.07 320);--b:oklch(0.55 0.13 340)"><figure></figure><figcaption>Neon district<span>Osaka · 2024</span></figcaption></a>
      <a class="cpl-03__piece" href="#p5" style="--img:url('https://images.unsplash.com/photo-1439066615861-d1af74d74000?q=80&w=900&auto=format&fit=crop');--r:1/1;--a:oklch(0.86 0.05 200);--b:oklch(0.62 0.09 230)"><figure></figure><figcaption>Still harbour<span>Oslo · 2026</span></figcaption></a>
      <a class="cpl-03__piece" href="#p6" style="--img:url('https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?q=80&w=900&auto=format&fit=crop');--r:3/4;--a:oklch(0.8 0.1 40);--b:oklch(0.52 0.15 25)"><figure></figure><figcaption>Red canyon<span>Utah · 2024</span></figcaption></a>
      <a class="cpl-03__piece" href="#p7" style="--img:url('https://images.unsplash.com/photo-1537996194471-e657df975ab4?q=80&w=900&auto=format&fit=crop');--r:4/3;--a:oklch(0.84 0.06 130);--b:oklch(0.55 0.1 150)"><figure></figure><figcaption>Terrace lines<span>Bali · 2025</span></figcaption></a>
      <a class="cpl-03__piece" href="#p8" style="--img:url('https://images.unsplash.com/photo-1506905925346-21bda4d32df4?q=80&w=900&auto=format&fit=crop');--r:1/1;--a:oklch(0.77 0.09 280);--b:oklch(0.48 0.14 300)"><figure></figure><figcaption>Blue passage<span>Chefchaouen · 2026</span></figcaption></a>
      <a class="cpl-03__piece" href="#p9" style="--img:url('https://images.unsplash.com/photo-1500382017468-9049fed747ef?q=80&w=900&auto=format&fit=crop');--r:3/4;--a:oklch(0.88 0.04 80);--b:oklch(0.65 0.08 100)"><figure></figure><figcaption>Wheat, late June<span>Alberta · 2025</span></figcaption></a>
    </div>
  </div>
</section>
.cpl-03,
.cpl-03 *,
.cpl-03 *::before,
.cpl-03 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cpl-03 {
  --bg: oklch(0.22 0.015 270);
  --ink: oklch(0.95 0.005 90);
  --mut: oklch(0.68 0.015 270);
  --line: oklch(0.32 0.02 270);
  --acc: oklch(0.8 0.14 85);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  container-type: inline-size;
}

.cpl-03__stage {
  width: 100%;
  background: var(--bg);
  padding: clamp(18px,4cqi,30px);
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.cpl-03__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 14px;
  margin-bottom: 20px;
}

.cpl-03__head h2 {
  font-size: 24px;
  letter-spacing: -.02em;
}

.cpl-03__head p {
  font-size: 12.5px;
  color: var(--mut);
}

.cpl-03__wall {
  columns: 3 220px;
  column-gap: 14px;
}

.cpl-03__piece {
  position: relative;
  display: block;
  break-inside: avoid;
  margin-bottom: 14px;
  border-radius: 14px;
  overflow: hidden;
  text-decoration: none;
  color: inherit;
}

.cpl-03__piece figure {
  aspect-ratio: var(--r);
  background-image: var(--img,none),linear-gradient(145deg,var(--a),var(--b));
  background-size: cover;
  background-position: center;
  transition: scale .55s cubic-bezier(.2,.7,.2,1);
}

.cpl-03__piece:hover figure,
.cpl-03__piece:focus-visible figure {
  scale: 1.05;
}

.cpl-03__piece figcaption {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 26px 14px 12px;
  font-size: 13.5px;
  font-weight: 600;
  background: linear-gradient(to top,oklch(0.15 0.02 270/.85),transparent);
  opacity: 0;
  translate: 0 10px;
  transition: opacity .3s,translate .35s cubic-bezier(.2,.7,.2,1);
}

.cpl-03__piece figcaption span {
  font-size: 11px;
  font-weight: 400;
  color: oklch(0.82 0.02 90);
}

.cpl-03__piece:hover figcaption,
.cpl-03__piece:focus-visible figcaption {
  opacity: 1;
  translate: 0 0;
}

.cpl-03__piece:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
}

@supports (grid-template-rows: masonry) {
  .cpl-03__wall {
    display: grid;
    columns: auto;
    grid-template-columns: repeat(auto-fill,minmax(min(220px,100%),1fr));
    grid-template-rows: masonry;
    gap: 14px;
  }

  .cpl-03__piece {
    margin-bottom: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cpl-03 figure,
    .cpl-03 figcaption {
    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 Portfolio 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 Masonry Portfolio Layout No JavaScript
Source: https://codefronts.com/layouts/css-portfolio-layouts/css-masonry-portfolio-layout-no-javascript/

A Pinterest-style wall of mixed-height pieces with zero JavaScript and zero libraries: CSS multi-columns do the packing, break-inside:avoid keeps every card whole, and captions slide up over the art on hover or keyboard focus. Column count is fluid — columns: 3 220px means 'up to three, never narrower than 220px' — so the wall re-packs itself for any container it lands in.
## HTML
```html
<section class="cpl-03" aria-label="CSS masonry portfolio layout demo">
  <div class="cpl-03__stage">
    <header class="cpl-03__head">
      <h2>Field Notes</h2>
      <p>35mm &amp; medium format — no JavaScript, no libraries</p>
    </header>
    <div class="cpl-03__wall">
      <a class="cpl-03__piece" href="#p1" style="--img:url('https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?q=80&w=900&auto=format&fit=crop');--r:3/4;--a:oklch(0.8 0.08 250);--b:oklch(0.45 0.1 270)"><figure></figure><figcaption>Fog over Hayes<span>SF · 2026</span></figcaption></a>
      <a class="cpl-03__piece" href="#p2" style="--img:url('https://images.unsplash.com/photo-1472214103451-9374bd1c798e?q=80&w=900&auto=format&fit=crop');--r:1/1;--a:oklch(0.85 0.09 90);--b:oklch(0.6 0.13 60)"><figure></figure><figcaption>Amber hour<span>Lisbon · 2025</span></figcaption></a>
      <a class="cpl-03__piece" href="#p3" style="--img:url('https://images.unsplash.com/photo-1505118380757-91f5f5632de0?q=80&w=900&auto=format&fit=crop');--r:4/5;--a:oklch(0.78 0.1 160);--b:oklch(0.5 0.11 190)"><figure></figure><figcaption>Sea wall study<span>Cornwall · 2025</span></figcaption></a>
      <a class="cpl-03__piece" href="#p4" style="--img:url('https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?q=80&w=900&auto=format&fit=crop');--r:3/2;--a:oklch(0.82 0.07 320);--b:oklch(0.55 0.13 340)"><figure></figure><figcaption>Neon district<span>Osaka · 2024</span></figcaption></a>
      <a class="cpl-03__piece" href="#p5" style="--img:url('https://images.unsplash.com/photo-1439066615861-d1af74d74000?q=80&w=900&auto=format&fit=crop');--r:1/1;--a:oklch(0.86 0.05 200);--b:oklch(0.62 0.09 230)"><figure></figure><figcaption>Still harbour<span>Oslo · 2026</span></figcaption></a>
      <a class="cpl-03__piece" href="#p6" style="--img:url('https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?q=80&w=900&auto=format&fit=crop');--r:3/4;--a:oklch(0.8 0.1 40);--b:oklch(0.52 0.15 25)"><figure></figure><figcaption>Red canyon<span>Utah · 2024</span></figcaption></a>
      <a class="cpl-03__piece" href="#p7" style="--img:url('https://images.unsplash.com/photo-1537996194471-e657df975ab4?q=80&w=900&auto=format&fit=crop');--r:4/3;--a:oklch(0.84 0.06 130);--b:oklch(0.55 0.1 150)"><figure></figure><figcaption>Terrace lines<span>Bali · 2025</span></figcaption></a>
      <a class="cpl-03__piece" href="#p8" style="--img:url('https://images.unsplash.com/photo-1506905925346-21bda4d32df4?q=80&w=900&auto=format&fit=crop');--r:1/1;--a:oklch(0.77 0.09 280);--b:oklch(0.48 0.14 300)"><figure></figure><figcaption>Blue passage<span>Chefchaouen · 2026</span></figcaption></a>
      <a class="cpl-03__piece" href="#p9" style="--img:url('https://images.unsplash.com/photo-1500382017468-9049fed747ef?q=80&w=900&auto=format&fit=crop');--r:3/4;--a:oklch(0.88 0.04 80);--b:oklch(0.65 0.08 100)"><figure></figure><figcaption>Wheat, late June<span>Alberta · 2025</span></figcaption></a>
    </div>
  </div>
</section>
```
## CSS
```css
.cpl-03,
.cpl-03 *,
.cpl-03 *::before,
.cpl-03 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cpl-03 {
  --bg: oklch(0.22 0.015 270);
  --ink: oklch(0.95 0.005 90);
  --mut: oklch(0.68 0.015 270);
  --line: oklch(0.32 0.02 270);
  --acc: oklch(0.8 0.14 85);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
  container-type: inline-size;
}

.cpl-03__stage {
  width: 100%;
  background: var(--bg);
  padding: clamp(18px,4cqi,30px);
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.cpl-03__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 14px;
  margin-bottom: 20px;
}

.cpl-03__head h2 {
  font-size: 24px;
  letter-spacing: -.02em;
}

.cpl-03__head p {
  font-size: 12.5px;
  color: var(--mut);
}

.cpl-03__wall {
  columns: 3 220px;
  column-gap: 14px;
}

.cpl-03__piece {
  position: relative;
  display: block;
  break-inside: avoid;
  margin-bottom: 14px;
  border-radius: 14px;
  overflow: hidden;
  text-decoration: none;
  color: inherit;
}

.cpl-03__piece figure {
  aspect-ratio: var(--r);
  background-image: var(--img,none),linear-gradient(145deg,var(--a),var(--b));
  background-size: cover;
  background-position: center;
  transition: scale .55s cubic-bezier(.2,.7,.2,1);
}

.cpl-03__piece:hover figure,
.cpl-03__piece:focus-visible figure {
  scale: 1.05;
}

.cpl-03__piece figcaption {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 26px 14px 12px;
  font-size: 13.5px;
  font-weight: 600;
  background: linear-gradient(to top,oklch(0.15 0.02 270/.85),transparent);
  opacity: 0;
  translate: 0 10px;
  transition: opacity .3s,translate .35s cubic-bezier(.2,.7,.2,1);
}

.cpl-03__piece figcaption span {
  font-size: 11px;
  font-weight: 400;
  color: oklch(0.82 0.02 90);
}

.cpl-03__piece:hover figcaption,
.cpl-03__piece:focus-visible figcaption {
  opacity: 1;
  translate: 0 0;
}

.cpl-03__piece:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
}

@supports (grid-template-rows: masonry) {
  .cpl-03__wall {
    display: grid;
    columns: auto;
    grid-template-columns: repeat(auto-fill,minmax(min(220px,100%),1fr));
    grid-template-rows: masonry;
    gap: 14px;
  }

  .cpl-03__piece {
    margin-bottom: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .cpl-03 figure,
    .cpl-03 figcaption {
    transition-duration: .01ms !important;
  }
}
```

How this works

Multi-column layout is the only CSS that packs variable-height items with no script and full browser support. Three declarations are the whole engine:

.wall{ columns: 3 220px; column-gap: 14px; }
.piece{ break-inside: avoid;      /* never split a card  */
        margin-bottom: 14px; }    /* the 'row' gap       */

The two-value columns shorthand is the responsive trick: it caps the count at 3 but lets the browser drop to 2 or 1 whenever a column would fall under 220px — no media queries, no container queries, nothing. Height variety comes from aspect-ratio on each cover (3/4, 1/1, 4/5, 3/2 …), which is exactly what real photography gives you.

One honest caveat, documented rather than hidden: columns order content down each column, not across rows — piece 2 sits under piece 1, not beside it. For a portfolio sorted 'best first' that's usually fine (your best work owns the tallest column), but when true row order matters the future-proof upgrade is native masonry — display:grid; grid-template-rows:masonry (behind flags today) — and this demo ships the @supports block ready to take over automatically the day it lands, with the columns version as the evergreen fallback. Captions ride a translate-only transition and appear on :hover and :focus-visible alike.

Make it yours

  • Column geometry: columns: 3 220px — raise the count for thumbnail walls, raise the min-width for editorial scale. Keep column-gap equal to the pieces' margin-bottom so gutters read square.
  • Aspect ratios: each piece's --r sets its shape; feed real <img> elements and delete the ratios — intrinsic image height drives the packing instead.
  • Caption trigger: captions currently reveal on hover/focus; make them always-on by removing the translate/opacity pair for a gallery-label look.
  • Order-sensitive walls: if row order is a hard requirement today, split pieces into N per-column wrappers server-side — the only no-JS way to force horizontal order until native masonry ships.

Gotchas — read before shipping

  • Without break-inside:avoid a card straddles two columns, slicing its rounded corners in half — the single most common masonry bug.
  • Never use vertical margin-top on pieces: the first item in each column would carry a phantom gap. Bottom margins only.
  • Columns create a new stacking context per column in some engines — dropdowns or tooltips inside pieces can clip; keep interactive overlays inside the piece box.
  • An <img> without width/height attributes (or aspect-ratio) makes columns visibly re-pack as images load — always reserve space to avoid layout shift (CLS).

Browser support

ChromeSafariFirefoxEdge
111+15.4+113+111+

Floor set by oklch() as this demo is written. The core technique itself goes back further — Chrome all.

CSS multi-column is 15+ years universal. The oklch() tokens set a 2023 floor — hex swaps remove it. grid-template-rows:masonry remains experimental (behind flags); the @supports block is inert until then.

Techniques used in this demo

Search CodeFronts

Loading…