Frontend System Design: Build a Pinterest-Style Infinite Grid

The Pinterest masonry grid is harder than it looks. Variable-height columns, infinite scroll, lazy-loaded images, no layout shift — all on a feed that may have thousands of items. The interview tests whether you understand the gnarly intersection of CSS layout, image loading, and virtualization.

Functional requirements

  • Multi-column masonry layout with variable heights
  • Infinite scroll with pagination
  • Lazy-loaded images
  • Click an image to open detail view
  • Responsive: column count changes with viewport width

Architecture choices

CSS Grid vs JS layout

CSS Grid with grid-template-columns: repeat(auto-fill, minmax(...)) is the simplest. But: CSS Grid does not produce true masonry — items in the same row align by the tallest. The result is awkward gaps.

True masonry requires either:

  • CSS grid-template-rows: masonry: the new spec, but Safari-only as of 2026. Not production-ready.
  • JS-based layout: compute item positions manually, position absolutely. Industry standard.

JS layout: column algorithm

Maintain N columns (number based on viewport width). For each new item:

  1. Find the shortest column
  2. Place the item at the bottom of that column
  3. Update that column’s running height

Each item gets position: absolute; left: ... ; top: ... — the container has explicit height = max column height.

Virtualization

Off-screen items are not rendered. Use intersection observer to determine which range of items is visible (with overscan). Maintain placeholder space for unrendered items so the scrollbar accuracy is preserved.

Library: react-virtualized supports masonry. TanStack Virtual is more flexible but masonry support is custom.

Image lazy loading

  • Native loading="lazy" handles most cases — well-supported, zero JS
  • For finer control (preload near-viewport images, fade-in animation), use IntersectionObserver
  • Always provide explicit width and height attributes to reserve space

Preventing CLS (Cumulative Layout Shift)

Pinterest is famous for being a CLS minefield. Without care, every image load shifts items below.

Mitigations:

  • Server returns image dimensions with the manifest (width, height per pin)
  • Client reserves the exact box before the image loads
  • Use aspect-ratio CSS property where supported
  • Background placeholder (low-res blurred preview) renders while full-res loads

Detail view

Tap on a pin → modal or new route. Modal preserves scroll position; new route requires explicit “back” handling. For Pinterest specifically, use a modal that overlays without unmounting the grid.

Resize handling

When viewport width changes:

  1. Recompute number of columns
  2. Re-layout all items
  3. Use ResizeObserver, debounce 100–300ms

Layout recomputation can be expensive — keep heights cached per item to avoid re-measuring.

Performance budget

  • First Contentful Paint < 1.5s
  • Largest Contentful Paint < 2.5s
  • CLS < 0.05
  • Time to Interactive < 3.5s

Frequently Asked Questions

Why not use the native CSS masonry spec?

It is in Safari and behind a flag in Chromium as of 2026. Production code still uses JS layout for cross-browser support.

How do you handle a pin that loads as a different size than expected?

Re-layout from that pin downward. Bound the visible items to avoid jarring scroll jumps. Most apps update only after the user scrolls past the affected pin.

How does Pinterest handle very long pins?

Pin heights are server-capped at a max ratio (e.g., 1:3.5). Anything longer is rendered with a “view full” CTA.

Scroll to Top