EmpireUI
Get Pro
← Blog8 min read#tailwind#masonry#layout

Masonry Layout in Tailwind: columns-* and the Pinterest Grid

Build Pinterest-style masonry grids with Tailwind's columns-* utilities — no JavaScript required. Gaps, breakpoints, and the CSS grid masonry fallback explained.

Code editor showing Tailwind CSS masonry grid layout columns

What "Masonry" Actually Means (and Why It's Still a Pain)

Masonry layout is deceptively simple to describe: items pack into columns like bricks on a wall, each one sitting directly below the shortest column rather than aligning to a fixed row. Pinterest made it famous in 2010. Sixteen years later, native CSS still doesn't ship it in every browser without a flag.

That's the frustrating reality. The CSS Grid spec has had grid-template-rows: masonry in draft since 2020, but as of mid-2026 it's behind a flag in Firefox and slowly rolling out in Chrome 127+. You can't ship it to production yet without a polyfill or a different approach entirely.

So what do you actually reach for? Two real options: Tailwind's columns-* utilities (pure CSS multi-column layout), or a JavaScript library like Masonry.js. Honestly, columns-* covers 90% of use cases and ships zero JS. That's the one we're focusing on today.

Worth noting: multi-column layout and CSS Grid masonry are different specs. columns-* flows content top-to-bottom within each column (like newspaper columns), which means your items render in reading order — not left-to-right across the row. For most galleries and card grids, that's completely fine. For strict left-to-right ordering (think a ranked list), you'd need JS.

The columns-* Utilities: What You Get Out of the Box

Tailwind maps directly to the CSS column-count and column-width properties via two families of utilities. columns-{n} sets a fixed column count (1–12, plus auto), and columns-{size} sets a minimum column width using your spacing scale — things like columns-xs, columns-sm, columns-72, columns-3xl. The browser then figures out how many columns fit.

Here's a basic Pinterest-style image grid you can drop in right now: ``html <div class="columns-1 sm:columns-2 lg:columns-3 xl:columns-4 gap-4"> <img class="mb-4 w-full rounded-xl" src="/img/a.jpg" /> <img class="mb-4 w-full rounded-xl" src="/img/b.jpg" /> <img class="mb-4 w-full rounded-xl" src="/img/c.jpg" /> <img class="mb-4 w-full rounded-xl" src="/img/d.jpg" /> <!-- ... --> </div> ` The gap-4 sets the column gutter (16px). The mb-4` on each image creates vertical spacing between cards within a column. That's it — real masonry with four lines of Tailwind.

One thing trips people up: gap-* on a columns-* parent only controls the column gap, not the row gap. Since items flow inside columns (not a grid), there's no separate row-gap. You control vertical rhythm with mb-* on each child. Set mb-4 on each card and you get consistent 16px gaps top-to-bottom.

In practice, columns-3xs through columns-xl (the fixed-width variants) are underrated. Instead of columns-3, you write columns-72 and the browser fits as many 288px columns as possible — responsive without a single breakpoint modifier. Genuinely useful for dynamic grids where you don't know the container width ahead of time.

Building a Real Card Grid: Avoiding the Break-Inside Bug

The single most common columns layout bug: a card gets sliced in half across a column boundary. You'll see it immediately with cards that have padding, borders, or box shadows — the card visually breaks mid-element. The fix is one class: break-inside-avoid.

Here's a complete card masonry example with the fix applied: ``html <div class="columns-1 sm:columns-2 lg:columns-3 gap-6 p-6"> <div class="break-inside-avoid mb-6 rounded-2xl bg-white p-5 shadow-md dark:bg-zinc-900"> <img class="mb-3 w-full rounded-xl" src="/img/photo.jpg" alt="..." /> <h3 class="text-lg font-semibold">Card Title</h3> <p class="mt-1 text-sm text-zinc-500">Some description text that can span multiple lines.</p> </div> <!-- repeat --> </div> ` break-inside-avoid maps to break-inside: avoid in CSS, telling the browser not to split this element across columns. Combine it with mb-6 (not gap-y`) for vertical spacing between cards.

Look, break-inside-avoid is non-negotiable the moment your cards have any visual boundary — border, shadow, background color. Without it you'll get phantom half-cards on some viewport widths and it'll look broken in production even if it looked fine in your 1440px monitor dev environment.

One more thing — break-inside-avoid doesn't work on older browsers that use -webkit-column-break-inside instead. If you need Safari 14 or earlier support (rare, but some enterprise apps), add a @layer utilities override in your tailwind.config.js or use a PostCSS plugin. For anything shipping in 2026, you're fine with the standard class.

Quick aside: if you're building glassmorphism components or frosted-card UIs, the masonry columns layout pairs extremely well. Each card can have its own backdrop-blur and bg-white/10 and the layout handles the variable heights without any JS measurement.

Responsive Masonry: Breakpoints and the Fixed-Width Trick

The responsive story with columns-* is straightforward. Stack modifier prefixes the same way you do everywhere in Tailwind: ``html <div class="columns-1 sm:columns-2 md:columns-3 xl:columns-4 gap-5"> <!-- cards --> </div> ` Mobile gets a single column, tablet gets two, desktop gets three or four. No media query boilerplate, no CSS files, no JavaScript ResizeObserver` watching container width.

The fixed-width variant is actually smarter for image galleries. Instead of hardcoding column counts per breakpoint, let the browser calculate: ``html <div class="columns-[280px] gap-4"> <!-- browser fits as many 280px columns as possible --> </div> ` This uses Tailwind's arbitrary value syntax. The browser squeezes in as many 280px columns as the container allows and distributes remaining space. You get implicit responsiveness — no need to specify every breakpoint. It's closer to how CSS Grid's auto-fill with minmax` works, but for the columns model.

That said, there's a real tradeoff. Fixed-width columns can produce an ugly single wide column on mid-range viewports if you pick the wrong base size. Test at 600px, 768px, and 1024px specifically — those are the widths where things fall apart. A min() approach in arbitrary values can help: columns-[min(280px,100%)].

For most content sites and blogs, the responsive breakpoint approach (columns-1 sm:columns-2 lg:columns-3) is more predictable. Use fixed-width columns when you're building a dynamic photo gallery where the content count is unknown and you want natural reflow.

CSS Grid Masonry: The Native Future (and How to Use It Today)

The spec-compliant future is grid-template-rows: masonry. It solves the ordering problem that columns-* has — items fill left-to-right across the row, stacking under the shortest column, while staying in DOM order. No JS. Real masonry semantics.

Here's what the CSS looks like when it's available: ``css .masonry-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: masonry; gap: 1.5rem; } ` In Tailwind terms, since there's no built-in utility for masonry rows yet, you'd drop this into a custom class via @layer utilities in your CSS: `css @layer utilities { .rows-masonry { grid-template-rows: masonry; } } ` Then use it as <div class="grid grid-cols-3 rows-masonry gap-6">`.

The catch: as of September 2026, Chrome 127+ has it in a flag under chrome://flags/#enable-experimental-web-platform-features, and Firefox has shipped it behind layout.css.grid-template-masonry-value.enabled. Safari has it under consideration in their tracker. You can't reliably ship this to production users without feature detection.

A pragmatic approach is progressive enhancement with @supports: ``css @layer utilities { .masonry-grid { columns: 3; gap: 1.5rem; } @supports (grid-template-rows: masonry) { .masonry-grid { columns: unset; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: masonry; } } } `` Columns layout as baseline, CSS Grid masonry as enhancement. Both look great — the only visual difference is item ordering.

Honestly, for 2026 production apps, stick with columns-* unless you have a hard requirement for left-to-right ordering. The native spec will be everywhere by 2027 or 2028. Build with the @supports pattern now and your upgrade path is one CSS rule removal.

Performance Considerations and Image Loading

Masonry layouts and image performance have a tense relationship. The browser doesn't know image heights until they load, which can cause layout shifts — exactly what CLS (Cumulative Layout Shift) measures. For a columns-* layout, this is actually less of a problem than CSS Grid, because columns reflow gracefully as images load rather than punching holes in a grid.

That said, you still want aspect ratio placeholders. Use aspect-video, aspect-square, or a custom aspect-[3/4] on image wrappers to reserve space before the image loads: ``html <div class="break-inside-avoid mb-4"> <div class="aspect-[3/4] overflow-hidden rounded-xl bg-zinc-100"> <img class="h-full w-full object-cover" src="/img/photo.jpg" loading="lazy" decoding="async" alt="..." /> </div> </div> ` If your images have variable aspect ratios (which is the whole point of masonry), skip the aspect-* wrapper and use width: 100%; height: auto on the img` directly. Accept a bit of layout shift — it's usually only visible on first load above the fold.

For galleries with dozens of images, pair your masonry layout with loading="lazy" and decoding="async" on every image below the fold. This ships by default in modern browsers and costs you nothing — just add the attributes. For above-the-fold images (the first 4–6 in the grid), use loading="eager" or omit the attribute entirely so they don't get delayed.

Worth noting: if you're running a Next.js app and using <Image> from next/image, you get automatic lazy loading, WebP conversion, and responsive srcset generation. The masonry pattern works exactly the same — just drop <Image> in place of <img> and add className instead of class.

Putting It Together: A Full Tailwind Masonry Gallery Component

Here's a production-ready gallery component in React with Tailwind. It handles responsive columns, break-inside, lazy loading, and a hover overlay — the whole stack: ``tsx type GalleryImage = { src: string; alt: string; caption?: string; }; export function MasonryGallery({ images }: { images: GalleryImage[] }) { return ( <div className="columns-1 gap-4 sm:columns-2 lg:columns-3 xl:columns-4"> {images.map((img, i) => ( <div key={i} className="group relative mb-4 break-inside-avoid overflow-hidden rounded-2xl" > <img src={img.src} alt={img.alt} loading={i < 4 ? 'eager' : 'lazy'} decoding="async" className="w-full transition-transform duration-300 group-hover:scale-105" /> {img.caption && ( <div className="absolute inset-x-0 bottom-0 translate-y-full bg-black/60 p-3 text-sm text-white transition-transform duration-300 group-hover:translate-y-0"> {img.caption} </div> )} </div> ))} </div> ); } ` The group-hover:scale-105` on the image and the sliding caption overlay are pure Tailwind — zero custom CSS. The first four images load eagerly, everything else is lazy.

You can extend this with Empire UI's card primitives or drop it into any of the templates as a section block. The component itself is stateless and purely presentational — easy to wrap with Suspense for streaming SSR in Next.js App Router.

For more visual flair, check out the glassmorphism generator to generate a frosted-glass card style you can paste directly onto your masonry cards. The backdrop-blur + semi-transparent background approach works beautifully over image grids — just make sure the card sits above the image rather than containing it.

One last thing: if you need a visual box shadow generator to dial in card elevation, that tool lets you export Tailwind utility classes directly. No more guessing at shadow-md vs shadow-xl — you see the actual rendered shadow before you commit.

What makes columns-* genuinely underrated is how little ceremony it requires. No third-party library, no ResizeObserver, no JavaScript at all. For the vast majority of galleries, blogs, and card grids, it's the right tool — ship it, move on.

FAQ

Does Tailwind have a built-in masonry grid utility?

Not a dedicated one, but the columns-* utilities produce masonry-style layouts natively. For CSS Grid masonry (grid-template-rows: masonry), you'd need a custom utility in @layer utilities since browser support is still experimental in 2026.

Why are my cards splitting across columns in Tailwind?

Add break-inside-avoid to each card element. Without it, the browser can split a card across a column boundary. This maps to break-inside: avoid in CSS.

How do I control the gap between masonry items in Tailwind?

Use gap-{n} on the columns container for horizontal (column) gaps, and mb-{n} on each child for vertical spacing. There's no row-gap in multi-column layout since items flow within columns.

Is CSS Grid masonry ready for production in 2026?

Not yet. As of September 2026, grid-template-rows: masonry is behind experimental flags in Chrome and Firefox, with no stable Safari support. Use columns-* for production and add @supports progressive enhancement for the Grid spec.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Tailwind Dashboard Layout: Sidebar, Header and Content GridBlog Layout in Tailwind: Article Page, Card Grid, SidebarMasonry Grid in React: CSS columns vs JavaScript Grid LayoutCSS Spacing System: 8px Grid, rem vs px, and Container Queries