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 — items keep their natural aspect ratio, so each column ends at a different height instead of lining up row by row. Interviewers check whether you place items by shortest column rather than filling left to right.
- Infinite scroll with pagination — fetch the next page as the user nears the bottom, using a cursor or offset the server hands back. Trigger the request before the sentinel is on screen so new pins are ready by the time the user reaches them.
- Lazy-loaded images — request an image only when it is near the viewport. On a feed of thousands of pins this cuts initial bandwidth and speeds up first paint.
- Click an image to open detail view — open the pin without losing the reader’s place in the grid. Expect a follow-up on how the URL and the browser back button should behave.
- Responsive: column count changes with viewport width — two columns on a phone, five or six on a wide screen. Compute the count from the container width, not the raw window size, so the grid still works when it lives inside a narrower layout.
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:
- Find the shortest column — track each column’s running height and pick the minimum, which keeps the bottom edges of the columns roughly even.
- Place the item at the bottom of that column — its left offset is the column index times the column width; its top offset is that column’s current running height.
- Update that column’s running height — add the item’s height plus the gutter so the next item dropped into this column lands directly below it.
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
widthandheightattributes 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-ratioCSS 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:
- Recompute number of columns — derive the new count from the current container width and your minimum column width.
- Re-layout all items — run the shortest-column pass again. Positions change, but cached item heights mean you never touch the DOM to re-measure.
- Use ResizeObserver, debounce 100–300ms — ResizeObserver fires on container size changes, not just window resizes, and debouncing keeps you from re-laying out on every intermediate pixel while the user drags the window edge.
Layout recomputation can be expensive — keep heights cached per item to avoid re-measuring.
Performance budget
- First Contentful Paint < 1.5s — time until the first text or image paints; keep the initial HTML and critical CSS small so the browser can render something fast.
- Largest Contentful Paint < 2.5s — time until the largest above-the-fold element (usually the tallest visible pin) renders; prioritize loading the first screen of images.
- CLS < 0.05 — total unexpected layout shift over the page’s life. Masonry grids fail this metric most, so reserve every image box before the image arrives.
- Time to Interactive < 3.5s — time until the main thread can respond to input; defer non-critical JS so an early scroll or tap isn’t blocked.
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.
Useful next steps:
