ESC

Type to search the knowledge base.

Design an Image Gallery

Frontend system design for an image gallery — grid, lightbox, responsive images, caching, and upload entry.

intermediate4 min read
  • system-design
  • interview
  • architecture
  • images

Scope the problem

In scope:

  • Responsive grid of images
  • Infinite scroll / pagination
  • Lightbox / detail view
  • Selection, share, download (as required)
  • Image delivery performance (srcset, CDN URLs)
  • Optional upload entry point

Out of scope: ML face recognition training, full DAM backend.

Requirements & metrics

Type Examples
Functional browse, open, navigate next/prev, zoom
Non-functional LCP, CLS from images; smooth scroll
Product time to first useful image; upload success

Architecture

GalleryPage
├── Toolbar (filters, sort, upload)
├── Virtualized or CSS grid
│   └── ImageTile (aspect box, blurhash)
├── Lightbox (dialog)
│   ├── Stage (img / picture)
│   └── Filmstrip
└── Image data cache (React Query)

Data model

type ImageAsset = {
  id: string;
  width: number;
  height: number;
  alt: string;
  blurHash?: string;
  // CDN templates or concrete URLs
  src: string;
  srcSet?: string;
  createdAt: string;
};

type GalleryPage = {
  items: ImageAsset[];
  nextCursor: string | null;
};

API: GET /images?cursor=&album= — cursor pagination.

Layout & virtualization

  • CSS grid with known aspect ratios (from width/height) prevents CLS
  • For 10k+ images: window virtualization (estimate row heights from aspect + column count)
  • Masonry: harder virtualization — mention tradeoff; uniform grid is safer at scale
<div style="aspect-ratio: 4/3">
  <img src="..." alt="..." loading="lazy" decoding="async" />
</div>

First row images: fetchpriority="high" on LCP candidate only.

Responsive images

<img
  alt="Sunset over lake"
  width="1600"
  height="900"
  srcset="
    https://cdn.example/img/abc?w=400 400w,
    https://cdn.example/img/abc?w=800 800w,
    https://cdn.example/img/abc?w=1600 1600w
  "
  sizes="(max-width: 600px) 50vw, 25vw"
  loading="lazy"
/>

Prefer modern formats via CDN (format=auto → AVIF/WebP/JPEG). See Media CDN and Image Pipeline.

  • role="dialog" + focus trap (modal pattern)
  • Prefetch next/prev full images
  • Keyboard arrows; Escape closes
  • Zoom: transform-based; don’t reload asset at every wheel tick
  • URL sync optional: /photos/:id for shareability (SSR meta/OG)

Caching

Layer What
HTTP CDN long-cache fingerprinted derivatives
Memory page query cache of listings
Browser disk cache of bytes
SW optional album shell offline

Don’t cache personalized “recommended” grids too aggressively without revalidation.

Upload path (if in scope)

  1. Select files → validate type/size client-side
  2. Show local previews (URL.createObjectURL)
  3. Direct-to-cloud multipart upload with progress
  4. Optimistic tile in grid with spinner
  5. Replace with server asset on complete

Large files: Upload Large Files UX.

Performance budgets

  • Thumbnails tiny (e.g. longest edge 400)
  • Avoid decoding full-res in grid
  • Use blurhash/LQIP for perceived speed
  • Limit concurrent decodes; virtualize

Accessibility

  • Meaningful alt (user-provided; fallback “Image”)
  • Tiles are buttons/links with names
  • Lightbox announced; focus managed
  • Don’t autoplay GIFs/videos without control

Tradeoffs

  1. Masonry vs uniform grid — aesthetics vs engineering cost
  2. Client waterfalls vs SSR first page for SEO albums
  3. SPA lightbox vs dedicated route
  4. Infinite scroll vs pages — SEO and “find item #900”

Interview close

Grid with aspect-ratio + CDN srcset → virtualization for scale → lightbox a11y + prefetch → cache layers → upload optimistic path. Call out CLS and LCP explicitly.

Selection & bulk actions

Power-user galleries need multi-select (shift-click range) for download/delete/add-to-album. Keep selection ids in a Set in URL or session — not only ephemeral React state — if users refresh mid-bulk.

Permissions

  • Public album vs private
  • Signed URLs for private full-res
  • Hide download if license forbids

Deep linking

/albums/:albumId/photos/:photoId opens lightbox with SSR for OG tags when shareable. On close, return to grid preserving scroll (session scroll restore keyed by album id).

Perf numbers to cite

  • Thumbnail longest edge ~200–400px
  • Concurrent decode: rely on browser; avoid 50 unthrottled full-res preloads
  • Lightbox: prefetch ±1 only unless on Wi‑Fi with spare bandwidth

Further reading