Frontend system design: design Pinterest
Masonry-grid pinboard with infinite scroll over cursor-paginated server feed; variable-height tiles requiring a packing algorithm (CSS columns or JS positioning); virtualization for long boards; aggressive image optimization with placeholders; pin/save interactions optimistic; SSR/SSG for SEO; accessible grid + keyboard nav.
Pinterest's distinctive feature is the masonry grid of variable-height image tiles. Most of the design follows from that constraint.
1. The masonry layout
Two approaches:
CSS columns (column-count/column-width) — simple, but items flow top-to-bottom in each column, which breaks visual reading order for many catalogs.
JS-positioned absolute tiles — compute the shortest column and place the next tile there. Required for real masonry where insertion order matters.
function placeTiles(tiles, columns, gap) {
const heights = new Array(columns).fill(0);
return tiles.map((tile) => {
const col = heights.indexOf(Math.min(...heights));
const pos = { left: col * (tileWidth + gap), top: heights[col] };
heights[col] += tile.height + gap;
return { ...tile, pos };
});
}CSS Grid + grid-auto-rows: 10px with each tile spanning rows proportional to its height is a modern hybrid.
2. Knowing tile height before render
You must know the image height before placing the tile — otherwise the grid jumps as images load.
- Server returns
{ url, width, height }for every pin. - Render a fixed-size placeholder (color or blurhash) at the known aspect ratio.
- Image swaps in when loaded; no layout shift.
3. Infinite scroll + virtualization
- Cursor-paginated feed (offset breaks under inserts).
- IntersectionObserver near the bottom triggers next-page fetch.
- For very long boards, virtualize — only render tiles in/near the viewport. With masonry, virtualization needs a height index per tile so you can map scroll position to visible range.
4. Image optimization (dominant cost)
- Responsive
srcsetwith multiple widths; CDN resizes on demand. - Modern formats: AVIF/WebP with fallback.
- Lazy-load below the fold (
loading="lazy"or IntersectionObserver). - Blurhash / LQIP placeholder so something is visible before the full image lands.
- Decode async to avoid main-thread blocks.
5. Interactions
- Save / Pin → optimistic toggle, board picker modal.
- Hover shows actions (desktop); long-press on mobile.
- Tap opens a pin detail page (route change; deep-linkable).
6. SEO & first paint
- SSR/SSG the home and category pages; first viewport's tiles render in HTML.
- Hydrate for interactivity.
- Pin detail pages are SSG with ISR.
7. Caching
- React Query keyed by board/cursor.
- Per-pin metadata cached aggressively (rarely changes).
- Service worker for offline-friendly browsing.
8. Accessibility
- Grid is a list of articles, not a literal CSS grid role —
role="list"+role="listitem"on tiles. - Every image needs alt text (server-supplied, with a fallback).
- Keyboard nav across tiles (Tab + arrow keys in some implementations).
- Focus management on opening a pin overlay.
9. Performance
- LCP is one of the above-the-fold tile images — preload it.
- Avoid layout thrash: place tiles in a single batched pass, not per-image-load.
- Code-split the pin detail / editor.
Interview framing
"The defining constraint is the masonry grid of variable-height image tiles. Server returns width/height with each pin so we render placeholders at the correct aspect ratio — no layout shift as images load. Tiles are placed by a shortest-column algorithm; in modern CSS, grid-auto-rows + row spans achieves the same. Cursor-paginated infinite scroll via IntersectionObserver; virtualize once boards get long. Image optimization is the dominant performance lever: srcset, AVIF/WebP, blurhash placeholders, lazy below-the-fold. SSR/SSG for SEO and LCP. Saves are optimistic. Accessibility: alt text on every image, accessible list semantics, keyboard nav."
Follow-up questions
- •Why does the server need to send width/height per pin?
- •CSS columns vs JS-positioned tiles vs CSS grid with row spans — trade-offs?
- •How do you virtualize a masonry grid?
- •What's a blurhash / LQIP and why use it?
Common mistakes
- •Placing tiles after images load — grid jumps everywhere (CLS).
- •CSS columns when insertion order matters — items go down columns.
- •Skipping virtualization on long boards.
- •No alt text / accessibility on an image-first product.
Performance considerations
- •Image bytes dominate; srcset + modern formats + lazy-load are the levers. Batched placement avoids reflow per-image-load. Virtualize long boards. Preload LCP tile.
Edge cases
- •Tile with unknown aspect ratio (no width/height) — render a fallback.
- •Window resize — recompute columns.
- •Very tall tiles dominating one column.
- •Network slow — placeholders carry the UX.
Real-world examples
- •Pinterest, Unsplash, Dribbble.
- •react-masonry-css and CSS Grid with row spans.