Design an Image Gallery
Frontend system design for an image gallery — grid, lightbox, responsive images, caching, and upload entry.
- 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.
Lightbox design
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/:idfor 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)
- Select files → validate type/size client-side
- Show local previews (
URL.createObjectURL) - Direct-to-cloud multipart upload with progress
- Optimistic tile in grid with spinner
- 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
- Masonry vs uniform grid — aesthetics vs engineering cost
- Client waterfalls vs SSR first page for SEO albums
- SPA lightbox vs dedicated route
- 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