Skip to content
WANCRAFT

B.130Cards & Containers

Masonry Grid

A measured masonry layout with responsive column breakpoints that springs tiles into place as they are added, removed or resized.

NewReactMotionTailwindintermediate

Overview

Masonry Grid lays out tiles of any height in columns, placing each one into the currently shortest column. The layout is computed in JavaScript from measured heights rather than CSS columns, so placement is stable and the DOM order stays the reading order instead of running down one column at a time. The column count follows the container's width through breakpoints (or a fixed number), and every tile is watched by a ResizeObserver, so images that load late, text that wraps and content that expands all trigger a re-measure. Tiles are absolutely positioned and spring to their new places on reflow, new tiles scale in and removed tiles fade out, and the first placement happens before paint so nothing jumps on load. Before JavaScript runs, tiles flow in CSS columns, so the content is never hidden.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/masonry-grid.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/masonry-grid.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/masonry-grid.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { MasonryGrid } from "@/components/ui/masonry-grid";

export function MasonryGridExample() {
  const photos = [
    { id: "a", src: "/photos/a.jpg", alt: "Harbour at dawn" },
    { id: "b", src: "/photos/b.jpg", alt: "Concrete stairwell" },
    { id: "c", src: "/photos/c.jpg", alt: "Fern detail" },
  ];

  return (
    <MasonryGrid columns={{ 0: 1, 480: 2, 720: 3 }}>
      {photos.map((photo) => (
        <img key={photo.id} src={photo.src} alt={photo.alt} className="block w-full rounded-xl" />
      ))}
    </MasonryGrid>
  );
}

03Props

PropTypeDefaultDescription
childrenReactNode—Required. Keyed elements, one per tile; the key identifies a tile across reorders.
columnsnumber | Record<number, number>{ 0: 1, 480: 2, 720: 3, 960: 4 }Fixed count, or column counts keyed by minimum container width in px.
gapnumber16Horizontal and vertical space between tiles, in px.
animatebooleantrueSpring tiles to new places and fade tiles in and out.
spring{ stiffness; damping; mass }{ 320, 32, 1 }Spring for reflow.
as"div" | "ul""div"Render a list (ul > li) for collections.
...propsHTMLAttributes<HTMLElement>—Forwarded to the root (aria-label, className…).

04Features

  • 01Shortest-column placement from measured heights, deterministic and order-stable
  • 02Responsive column count from container-width breakpoints (not the viewport)
  • 03ResizeObserver on every tile: late images and expanding content re-flow automatically
  • 04Spring reflow, scale-in for new tiles and fade-out for removed ones
  • 05First placement measured before paint; CSS-columns fallback before hydration
  • 06Optional list semantics with as="ul"
motion · springmotion · layoutinput · clickinput · viewport

05Accessibility

  • DOM order is the order of your children, so keyboard focus and screen readers follow the data order, which matches the visual order row by row as closely as masonry allows.
  • Tiles are not reordered visually by CSS (no column flow after hydration), so tab order never jumps between columns.
  • as="ul" renders a real list with list items; label it with aria-label.
  • prefers-reduced-motion (or animate={false}) moves tiles instantly with no scale or fade.

06Limitations

  • Tiles are absolutely positioned, so the container's height is set from the layout; content outside the tiles' boxes (e.g. large shadows) can overlap the next element.
  • Very large collections should be virtualised; every tile is measured and rendered.