---
name: masonry-layout
description: Use when you want "Casual, editorial, content-heavy" - Arranges items in a Pinterest-like uneven grid.
---

# Masonry Layout

> **Category:** Card / Layout  -  **Personality:** Casual, editorial, content-heavy
>
> **Best use:** Galleries, blogs, portfolios
>
> **Ratings:** Professional 4/5 - Casual 5/5 - Exotic 2/5 - Visual impact 3/5 - Difficulty 2/5 - Performance cost 2/5

## What it is

A masonry layout packs items of **different heights** into vertical columns with no fixed rows, so each item slots in directly under the previous one in its column — the staggered "Pinterest wall" look. The simplest, most robust build is pure CSS using **`column-count`** (CSS multi-column) with **`break-inside: avoid`** so a card never splits across two columns. Reach for it whenever the content is visual and the heights are genuinely uneven — image galleries, blog feeds, portfolio grids, mood boards.

## Dependencies / CDN

**None - pure CSS** for the layout itself. JavaScript is optional and only powers the gallery extras (category filter + Save toggle); the masonry works with JS disabled.

The demo loads two Google Fonts (purely cosmetic):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
```

## HTML

A flat list of cards — no row wrappers. Each card sets its own display aspect-ratio via a `--ar` custom property so the tile heights vary:

```html
<div class="ms-grid">
  <figure class="ms-card" style="--ar:2/3">
    <img src="/img/portrait-01.jpg" alt="Backlit portrait" loading="lazy" decoding="async">
    <figcaption class="ms-cap">
      <h3 class="ms-pin">Golden hour, off the grid</h3>
      <span class="ms-by">@lenaverse</span>
    </figcaption>
  </figure>

  <figure class="ms-card" style="--ar:3/2"> ... </figure>
  <figure class="ms-card" style="--ar:1/1"> ... </figure>
  <!-- repeat with mixed aspect-ratios for a varied wall -->
</div>
```

## CSS

```css
/* 1) THE MASONRY — the only lines that matter for the effect */
.ms-grid{
  column-count: 4;        /* number of columns */
  column-gap: 16px;       /* horizontal gutter */
}
@media (max-width: 1080px){ .ms-grid{ column-count: 3 } }
@media (max-width: 760px) { .ms-grid{ column-count: 2; column-gap: 12px } }

.ms-card{
  break-inside: avoid;               /* never split a card across columns */
  -webkit-column-break-inside: avoid;/* older WebKit */
  margin: 0 0 16px;                  /* vertical gutter between stacked cards */
  display: block;
}

/* 2) Cards: reserve space with aspect-ratio + cover the box (gives varied heights, no CLS) */
.ms-card{
  position: relative; overflow: hidden; border-radius: 16px;
  aspect-ratio: var(--ar, 3/4);      /* per-card height; mix 2/3, 1/1, 3/2, 4/5 ... */
  background: #ece4d6;               /* placeholder tint while the image loads */
}
.ms-card img{
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;                 /* fill the tile, crop the overflow */
  display: block;
}
```

## JavaScript

Not required for layout. The demo adds two optional gallery touches: a category filter (the grid re-packs itself automatically when cards are hidden) and a Save toggle. All motion is gated behind `prefers-reduced-motion`.

```js
var grid  = document.getElementById('msGrid');
var cards = [].slice.call(grid.querySelectorAll('.ms-card'));
var chips = [].slice.call(document.querySelectorAll('.ms-chip'));
var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;

function layout(filter){
  var i = 0;
  cards.forEach(function(card){
    var show = filter === 'all' || card.dataset.cat === filter;
    card.classList.toggle('is-hidden', !show);   // display:none -> columns reflow
    if (show){
      if (!reduce){                              // staggered re-reveal
        card.style.setProperty('--i', i);
        card.classList.remove('ms-replay');
        void card.offsetWidth;                   // restart the CSS animation
        card.classList.add('ms-replay');
      }
      i++;
    }
  });
}

chips.forEach(function(chip){
  chip.addEventListener('click', function(){
    chips.forEach(function(c){ c.classList.remove('is-active'); });
    chip.classList.add('is-active');
    layout(chip.dataset.filter);
  });
});

// Save pill toggle (event delegation)
grid.addEventListener('click', function(e){
  var btn = e.target.closest('.ms-save');
  if (!btn) return;
  var saved = btn.classList.toggle('is-saved');
  btn.textContent = saved ? 'Saved' : 'Save';
  btn.setAttribute('aria-pressed', saved);
});

layout('all');
```

## How it works

- **`column-count` splits the container into N equal-width columns** and flows the cards into them top-to-bottom. Because each card keeps its own height, the columns end up uneven — that *is* the masonry look. No row alignment, no JS measuring.
- **`break-inside: avoid`** stops the multi-column algorithm from slicing a card in half at a column boundary. Without it, an image can be cut and continued in the next column.
- **`aspect-ratio` + `object-fit: cover`** give each tile a deliberate height and crop the photo to fill it cleanly. Mixing ratios (`2/3`, `1/1`, `3/2`, `4/5`) is what creates the lively, varied wall; it also reserves space before the image loads, so there is no layout shift.
- **Responsiveness is one line per breakpoint** — just change `column-count`. The cards re-flow with zero markup changes.

## Customization

- **Density:** raise/lower `column-count` (or swap to `column-width: 260px` to let the browser decide how many columns fit).
- **Gutters:** `column-gap` controls horizontal spacing; the card's `margin-bottom` controls vertical spacing — keep them equal for an even grid.
- **Tile heights:** drive each card's `--ar`. Want truly organic heights instead of curated crops? Drop `aspect-ratio`/`object-fit` and let `<img>` flow at `width:100%; height:auto` so every tile keeps its native ratio.
- **Reveal:** the `--i` index feeds `animation-delay: calc(var(--i) * 42ms)` for a staggered fade-in on load and on every filter change.

## Accessibility & performance

- Performance is cheap — it is layout-only, GPU-friendly, and scales to hundreds of cards. Add `loading="lazy"` + `decoding="async"` to images and always set `aspect-ratio` (or width/height) so the page does not reflow as photos arrive.
- Give every `<img>` a real `alt`. Hover-revealed captions must still be reachable: the demo reveals them on `:focus-within` too, and forces them visible under `@media (hover: none)` so touch users always see the text.
- Wrap all non-trivial motion in `@media (prefers-reduced-motion: reduce)` and re-check it in JS via `matchMedia` before animating (the filter still works, just without the stagger).
- Keep interactive controls as real `<button>`s with `aria-pressed`, not click-handlered `<div>`s.

## Gotchas

- **Reading order is column-major, not left-to-right.** CSS multi-column fills column 1 top-to-bottom, then column 2 — so visual order is *down* each column, which can scramble chronological feeds and tab/DOM order. If strict left-to-right order matters, use a JS masonry library or a future native `grid-template-rows: masonry`.
- **`break-inside: avoid` is mandatory** — omit it and cards get sliced at column boundaries. Some engines also need the legacy `-webkit-column-break-inside`.
- **Native CSS Grid masonry isn't ready.** `grid-template-rows: masonry` is still experimental/behind flags in 2026 — don't ship it as the primary path; multi-column or JS are the reliable choices today.
- **Avoid `100vw`/margin breakouts** inside the grid; size the container normally or you'll trigger horizontal scrollbars.
- **Margins collapse oddly across columns** in a few engines; prefer `margin-bottom` only (not top) on cards to keep gaps uniform.
