Skip to content

SSR Rendering ​

A masonry layout depends on real measured sizes, which don't exist on the server and don't exist for the one client frame before the engine's first measurement either. Rather than rendering nothing (or a flash of unstyled/overlapping content) during that window, every adapter renders a CSS columns approximation instead, then swaps to the exact transform-positioned layout the instant the engine measures for real — in the same frame, not a visible flash.

SSR Fallback Column Count ​

MasonryOptions.ssrColumns

ssrColumns ​

number · optional

Explicit column count for the pure-CSS columns approximation a framework adapter renders before the engine exists. The engine itself never reads this field — it's resolved via the core-exported resolveSsrColumns() helper and consumed entirely by the adapter, so it's meaningful to set even though it's not something createMasonryEngine() itself does anything with.

Resolution order (via resolveSsrColumns(spec, explicit)):

  1. An explicit ssrColumns always wins.
  2. Otherwise, if columns (or rows, at direction: 'horizontal') is already a concrete number or breakpoints object — no measurement needed to resolve those — it's reused as-is (a breakpoints object resolves to its default value, since the real container width isn't known yet).
  3. 'auto' or unset falls back to a conservative default of 2 — safe on both mobile and desktop widths, never uncomfortably narrow.
ts
const engine = createMasonryEngine(container, {
  columns: 'auto', // real column count resolved once the container is actually measured
  ssrColumns: 3, // but render a 3-column CSS-columns approximation until then
})

From the CSS Approximation to the Real Layout ​

Before the engine's first layout event fires, <MasonryGrid> renders its root element with:

css
columns: <resolved ssrColumns>;
column-gap: <resolved cross gap>px;

and each item wrapper gets break-inside: avoid plus margin-bottom: <main gap>px — a reasonable-looking, if not pixel-exact, masonry approximation using nothing but CSS the browser already understands, with zero JavaScript involved. This is what renders during SSR (no window, no engine at all) and for the one client frame before hydration's first real measurement completes.

The instant the engine's layout event fires for the first time, the adapter drops that CSS entirely (no columns/column-gap/break-inside left behind) and switches every item to its real position: absolute + transform: translate() placement — driven by a boolean flip (hasLaidOut) that Vue/React's own reactivity applies before the next paint, so there's no visible frame where the two layouts are both present or where content jumps between them.

No <ClientOnly>/useIsomorphicLayoutEffect-style wrapper is something you need to add yourself anywhere — both the Vue and React adapters already handle the server/client split internally (React's own useLayoutEffect warns if it runs on the server, so the adapter falls back to useEffect there automatically).