Skip to content
GetUrUI

Masonry layout: how to build a masonry grid in CSS and React

A masonry layout stacks items of different heights into tidy columns with no ragged gaps, like bricks in a wall. There are three ways to build one today, and they disagree about one thing that matters: the order people read your content in.

By GetUrUI team6 min read

The examples in this guide are our own products. Masonry Gallery and Testimonials are free and MIT licensed, Testimonial blocks are part of GetUrUI Pro, and templates are sold separately.

What is a masonry layout?

A masonry layout places items of equal width but different heights into columns, and packs each column tightly so there are no gaps under short items. The name comes from bricklaying, and the look is familiar from image boards and portfolio sites.

The difference from an ordinary grid is the row axis. A normal CSS grid lines items up in rows, so a short item leaves empty space next to its tall neighbour. A masonry grid has strict columns but no strict rows: each item sits directly under the one above it.

When to use a masonry grid

Masonry is the right call when you have many items whose heights you don't control:

  • Image galleries and portfolios with a mix of portrait, landscape and square shots you don't want to crop.
  • Testimonial walls, where some quotes are one line and others are a paragraph.
  • Mood boards, notes and cards from users, where content length varies.

It is the wrong call for a few curated items with a clear lead (a bento grid layout does that better), for content people compare row by row, like pricing or specs, and for anything that must be read in a strict sequence.

Three ways to build a masonry layout in CSS

1. CSS columns

Multi-column layout is the oldest trick and needs no JavaScript. The browser pours items into the first column, then the second, and balances the heights. break-inside: avoid stops a tile being split across two columns.

masonry-columns.css
1.masonry {
2  columns: 3 260px; /* up to 3 columns, none narrower than 260px */
3  column-gap: 16px;
4}
5
6.masonry > * {
7  break-inside: avoid; /* never split a tile across two columns */
8  margin-bottom: 16px;
9}

The catch is order. Items run down the first column before the second starts, so item 2 sits under item 1, not beside it. More on why that matters below.

2. CSS grid with measured row spans

This version keeps the source order left to right. The grid gets very short rows, and each tile spans as many rows as its height needs. The grid still places items in source order, and each tile lands level with or below the one before it, so the wall reads left to right and down; the spans just let tall tiles reach further.

masonry-grid.css
1.masonry-grid {
2  display: grid;
3  grid-template-columns: repeat(3, minmax(0, 1fr));
4  grid-auto-rows: 4px; /* tiny rows: each tile spans as many as it needs */
5  column-gap: 16px;
6  /* no row-gap: the space under each tile is part of its span */
7}
masonry-grid.js
1// Give each tile enough 4px rows for its height plus a 16px gap,
2// and update it whenever the tile's size changes (images loading, captions wrapping, resizes).
3const ROW = 4;
4const GAP = 16;
5
6for (const cell of document.querySelectorAll(".masonry-grid > li")) {
7  const inner = cell.firstElementChild;
8  new ResizeObserver(([entry]) => {
9    const rows = Math.ceil((entry.contentRect.height + GAP) / ROW);
10    cell.style.gridRowEnd = `span ${rows}`;
11  }).observe(inner);
12}

The cost is a little JavaScript, and heights that are estimated until it runs. Because the spans are measured rather than calculated, they stay right when an image loads late or a caption wraps onto two lines.

3. Native CSS masonry (grid lanes)

CSS Grid Level 3 defines masonry natively, as display: grid-lanes. Each item goes into the lane with the most room, with no JavaScript and no spans. At the time of writing, MDN marks it experimental and not Baseline: it doesn't work in some of the most widely used browsers. Check the compatibility table before you rely on it, and keep one of the first two methods as a fallback.

masonry-native.css
1/* Native masonry: experimental, not yet in every major browser. */
2.masonry-native {
3  display: grid-lanes;
4  grid-template-columns: repeat(3, minmax(0, 1fr));
5  gap: 16px;
6}
CSS columnsGrid + row spansNative grid lanes
JavaScriptNoneA ResizeObserver per tileNone
Reading orderDown each columnLeft to right, like the sourceLeft to right, like the source
Browser supportEvery modern browserEvery modern browserExperimental, not Baseline
Best forShort walls, quotes, simple galleriesGalleries where order mattersFuture projects, with a fallback

The reading-order trade-off

Screen readers and the Tab key follow the DOM. Sighted readers scan a masonry wall left to right. With CSS columns those two orders split: a keyboard user tabbing through a three-column gallery goes all the way down column one, jumps back to the top, and starts column two. WCAG's Meaningful Sequence criterion asks that the order a page is read in keeps its meaning.

  • Order doesn't matter (a wall of quotes, a mood board): CSS columns are fine.
  • Order matters (newest first, ranked results, a numbered series): use grid with row spans, or native grid lanes with a fallback.
  • Don't reorder the DOM to fake it. Splitting items into separate column lists in JavaScript makes the visual order look right but leaves keyboard order jumping between columns.

A masonry grid in Tailwind CSS and React

Tailwind has utilities for the columns method: columns-* and break-inside-avoid. This version uses container queries so the wall adapts to the space it is given, and passes each image's ratio so the browser can reserve its space before the image loads.

masonry.tsx
1type Shot = { title: string; src: string; alt: string; ratio: number };
2
3export function Masonry({ shots }: { shots: Shot[] }) {
4  return (
5    <section className="@container">
6      <ul className="columns-1 gap-4 @xl:columns-2 @4xl:columns-3">
7        {shots.map((s) => (
8          <li key={s.src} className="mb-4 break-inside-avoid">
9            <figure className="overflow-hidden rounded-lg border border-border bg-surface">
10              {/* aspect-ratio reserves the space before the image arrives: no layout shift */}
11              <img src={s.src} alt={s.alt} loading="lazy" style={{ aspectRatio: s.ratio }} className="w-full object-cover" />
12              <figcaption className="px-4 py-3 text-sm font-semibold text-ink">{s.title}</figcaption>
13            </figure>
14          </li>
15        ))}
16      </ul>
17    </section>
18  );
19}

Setting aspect-ratio from data you already have (image width ÷ height) means the wall doesn't jump as images arrive, which keeps layout shift low.

How we built our masonry walls

We use masonry in several places on GetUrUI, and not with the same technique everywhere. Here is what each one does and why, taken from the source you install.

Testimonial walls: CSS columns

The masonry variant of our free Testimonials block and the Pro Testimonial blocks masonry both use columns-1 @xl:columns-2 @4xl:columns-3 with break-inside-avoid. Quotes have no meaningful order, so pure CSS wins. In the Pro version a card can expand in place to show the longer story. Because multi-column layout rebalances when a card grows, neighbouring cards can shift between columns, so we animate their positions with Motion's layout="position" rather than letting them jump.

Masonry Gallery: grid with measured spans

For the Masonry Gallery block, order does matter: a portfolio usually shows newest work first, and a category filter should keep that order. So its default grid layout uses 4px rows, and each tile measures itself with a ResizeObserver. Our first version estimated each tile's height from its ratio plus a fixed caption height, but a caption that wraps onto two lines would outgrow its span. The shipped version measures the real height instead. The columns layout is still there for walls where order doesn't matter.

Things we kept in both

  • Each tile's space is reserved with aspect-ratio, so nothing jumps as images load.
  • Filter chips are real buttons with aria-pressed, and a polite live region says how many items are showing.
  • Reduced motion is respected. Tiles fade in without moving, and filtering doesn't animate positions.

Masonry examples you can install

These masonry examples are all from GetUrUI and install as source with the shadcn CLI. Each one is tagged Free or Pro, or with its template price.

Tiles of any aspect ratio on a grid with measured spans, so the order reads left to right. Category chips filter the wall, and the tiles glide into their new places. Works with your images or with drawn placeholders. MIT licensed. Masonry Gallery docs
$npx shadcn@latest add @geturui/masonry-gallery
A masonry wall of quotes in mixed card styles (metric cards, tinted cards and highlighted phrases) that expand in place to show the longer story. Testimonials docs
$npx shadcn@latest add @geturui-pro/testimonial-blocks-masonry

Install a masonry grid with the shadcn CLI

Add the theme tokens once, then the block. Pro items need your registry token; see Installation and Pro registry for the components.json setup.

terminal
1npx shadcn@latest add @geturui/theme
2npx shadcn@latest add @geturui/masonry-gallery                  # Free
3npx shadcn@latest add @geturui/testimonials-wall                 # Free
4npx shadcn@latest add @geturui-pro/testimonial-blocks-masonry    # Pro, needs GETURUI_TOKEN

FAQ

Can I make a masonry layout with only CSS?

Yes. CSS columns with break-inside: avoid work in every modern browser with no JavaScript. The trade-off is that items read down each column. Native display: grid-lanes is pure CSS too, but it is still experimental.

What is the difference between masonry and a bento grid?

Masonry fits many items of unknown height into equal-width columns. A bento grid is a designed layout of a few tiles whose sizes you choose. See our bento grid layout guide for that one.

Does a masonry layout hurt accessibility?

Only when the visual order and the DOM order disagree on content where order matters. Use a grid-based or native masonry for ordered content, and give images real alt text.

Is there a free masonry gallery component for React?

Yes. GetUrUI's Masonry Gallery is a free, MIT-licensed React and Tailwind CSS block, installed with npx shadcn@latest add @geturui/masonry-gallery.

Sources