Masonry

Lay out items of different heights in columns, each one placed in the shortest column or in order.

Items keep their own height and stack into columns without gaps, like a Pinterest board. Modeled on MUI's Masonry, and works in React, Vue, Svelte, Solid, Preact and vanilla JS.

  • Each item goes to the shortest column, or fills the columns in order
  • A fixed column count, a minimum column width, or a column count set from CSS per breakpoint
  • Items can span several columns
  • Re-lays out when an item resizes, an image loads, or items are added or removed
  • Renders as a plain grid until measured, so server-rendered markup reads fine before hydration
  • RTL support

Installation

in the preview, then copy the prompt into Claude Code, Cursor or another coding agent, or add the component yourself with the shadcn CLI or by hand.

Prompt for your coding agent
Show prompt
Add the masonry layout from isbatak-labs to this project. It lays out items of different heights in columns, placing each one in the shortest column or in order. Use the React version styled with Panda CSS and built on the Zag state machine. Docs: https://isbatak-labs.vercel.app/components/prototypes/masonry.md

1. Install it with the shadcn CLI. Swap `pnpm dlx` for `npx`, `yarn dlx` or `bunx` to match the project's lockfile:

pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/masonry-basic-react-panda.json

This installs `@isbatak/zag-masonry` and `@zag-js/react`, plus `@isbatak/panda-masonry` as a dev dependency, and adds the component to `src/components/masonry-basic/`. If the CLI can't run here, install those packages yourself and write the files listed in https://isbatak-labs.vercel.app/r/masonry-basic-react-panda.json (each entry has its content and target path).

2. Finish the setup:

- Panda CSS: add `"@isbatak/panda-masonry"` to the presets in the Panda config (or import `masonryPreset` from it if the config already imports its presets), then run `panda codegen`. If the project doesn't use Panda CSS, stop and tell me to pick the CSS version in the docs instead.

3. Ask me where the masonry should go and what it should show, and suggest a spot if one stands out. Then render it there: copy the installed component, give it a name that fits, replace the items with mine (each item needs a unique `value` and must be a direct child of the root), and keep the layout options that fit (`columns`, `gap`, `sequential`).

4. Start the dev server, check that the items render in columns without gaps and re-flow when the window is resized, then stop it. If you can't run it, tell me how to check it myself.
pnpm dlx shadcn@latest add https://isbatak-labs.vercel.app/r/masonry-basic-react-panda.json

This installs the dependencies and adds the component to src/components/masonry-basic/. It imports the recipe from styled-system/recipes.

Add "@isbatak/panda-masonry" to presets in your Panda config, or import masonryPreset from it and add that instead.

Examples

Scroll to an example to load it in the preview, and open the code view there for the full source. The settings button in the preview changes the example live, and the snippet under it follows. Each example is its own registry item, so you can add it with the CLI the same way.

Items must be direct children of the root, and each one needs a unique value.

Image masonry

Items can hold anything, images included. The layout runs again as each image loads, so lazy-loaded images don't need a known size up front. Items are placed in reading order, row by row.

const controller = masonry.createMasonry()
const api = controller.connect(
  {
    id,
    columns: 3,
  },
  layout,
  normalizeProps,
)

Items with variable height

Every item is watched for size changes. Open an accordion and the items below it move, and may switch columns, so each one still lands in the shortest column.

const controller = masonry.createMasonry()
const api = controller.connect(
  {
    id,
    columns: 3,
  },
  layout,
  normalizeProps,
)

Columns

Set columns for a fixed count. To change it per breakpoint, leave columns unset and set the --masonry-columns CSS variable on the root instead, here 3 columns on small screens and 4 from sm up. Set minColumnWidth, for example "12rem", to fit as many columns as there is room for.

const controller = masonry.createMasonry()
const api = controller.connect(
  {
    id,
  },
  layout,
  normalizeProps,
)

Spacing

Set gap to a number of pixels or a CSS length. Leave it unset to take the spacing from CSS, so it can change per breakpoint, here from 2 to 6. With Panda CSS, the recipe's gap variant picks from the spacing tokens.

const controller = masonry.createMasonry()
const api = controller.connect(
  {
    id,
    columns: 3,
  },
  layout,
  normalizeProps,
)

Sequential

Set sequential to fill the columns in order, left to right, instead of adding each item to the shortest column.

const controller = masonry.createMasonry()
const api = controller.connect(
  {
    id,
    columns: 4,
    sequential: true,
  },
  layout,
  normalizeProps,
)

Spanning columns

Set span on an item to make it cover several columns. It goes where the columns it covers are shortest, and the span is capped at the number of columns.

const controller = masonry.createMasonry()
const api = controller.connect(
  {
    id,
    columns: 3,
  },
  layout,
  normalizeProps,
)

Server-side rendering

Until the items are measured, the root is a plain CSS grid with the same columns and gap, so server-rendered markup shows every item in place without overlapping. After hydration the items are measured and moved into their columns, and the root gets a data-measured attribute. There's no need for a default height or column count.

Props

PropTypeDefault
idstring
The unique id of the masonry.
—
idsElementIds
The ids of the elements in the masonry. Useful for composition.
—
columnsnumber
The number of columns. Leave it unset to control it from CSS with the --masonry-columns variable, for example to change it per breakpoint.
4
minColumnWidthstring
The minimum column width, for example "12rem". When set, the root fits as many columns as there is room for and columns is ignored.
—
gapnumber | string
The space between items, in pixels or as a CSS length. Leave it unset to control it from CSS with gap.
—
sequentialboolean
Whether items fill the columns in order, left to right, instead of going to the shortest column.
false
onLayoutChange(details: LayoutChangeDetails) => void
Function called when the number of columns or the height changes.
—
dir"ltr" | "rtl"
The document's text/writing direction.
"ltr"

API

PropertyType
columnsnumber
The number of columns, or 0 before the first layout.
measuredboolean
Whether the items have been measured and placed. Before that they render as a plain grid.
reflow() => void
Measure and place the items again.