# Build an Image Gallery with Lightbox

Source: https://www.techinterview.org/post/3233475171/build-image-gallery-lightbox-frontend/
Updated: 2026-07-26 · techinterview.org

An image gallery with lightbox (thumbnails grid + tap-to-zoom modal) appears in many products. [The interview](/system-design-interview-guides/) tests virtualization, accessibility, gesture handling, and the polish that separates serviceable from delightful.

## Functional requirements

- Grid of thumbnails

- Tap a thumbnail to open lightbox

- Lightbox shows full image

- Navigate next/previous via buttons or swipe

- Pinch-to-zoom on full image

- Close via Escape, tap outside, or swipe down

- Smooth animation on open/close

## Architecture

Two views: **grid** (thumbnails) and **lightbox** (single image, navigable).

## Thumbnail grid

For a few hundred images: CSS Grid with srcset for responsive sizing.

For thousands: virtualize. Render only visible thumbnails.

Use `grid-template-columns: repeat(auto-fill, minmax(150px, 1fr))` for fluid columns.

## Image preloading

Thumbnails: `loading="lazy"` for off-screen.

Full images: preload on thumbnail hover or as user opens lightbox. Pre-fetch the next/previous in lightbox so swipe is instant.

## Lightbox open animation

The "shared element transition" pattern:

- User taps thumbnail

- Lightbox opens; the thumbnail visually expands to fill the screen

- Image loads at full resolution underneath

Implementation: use the View Transitions API (Chromium 2024+) or a library like react-spring with shared layout IDs.

For browsers without View Transitions: simpler fade-in is fine.

## Navigation in lightbox

- Buttons (left/right) on desktop — make them large and reveal them on hover so they don't clutter a clean view; disable or wrap at the first and last image, and pick one behavior deliberately rather than leaving it ambiguous.

- Swipe gesture on mobile — track the horizontal drag with PointerEvents and commit the change only past a distance or velocity threshold; below it, spring the current image back into place.

- Keyboard arrows on desktop — bind ArrowLeft and ArrowRight to previous/next, and preload the neighboring image so the jump lands with no visible loading.

- Indicator showing current position (3 of 47) — keep a live counter in a corner; it orients users in large sets and doubles as the text a screen reader can announce.

## Pinch-to-zoom

Mobile: PointerEvents to detect 2-finger pinch. Apply transform: scale().

Constraints:

- Min zoom: fit-to-screen — the zoomed-out state should exactly fit the viewport, and users shouldn't be able to pinch the image smaller than that.

- Max zoom: 4x or 8x — cap it against the source resolution so people can't zoom past the pixels you actually have and land on a blurry mess.

- Pan when zoomed — once scaled past 1x, dragging should move the image, clamped to its own bounds so you can't pan into empty margins.

- Tap to reset zoom — a double-tap toggles between fit and a focused zoom centered on the tap point, matching what native photo viewers do.

Use a library: react-zoom-pan-pinch, or CSS-only "Photo Swipe" if you want minimal JS.

## Closing the lightbox

- Escape key — the desktop default; wire it up before anything else because reviewers reach for it first.

- Tap outside the image — treat backdrop taps as close, but ignore taps that land on the image or controls so users don't dismiss by accident.

- Tap close button — an explicit X with a large tap target in a corner; it's the one path that works no matter which gestures a device supports.

- Swipe down (mobile) — a downward drag dismisses, giving mobile users the escape gesture they already expect from native photo apps.

The swipe-down close should follow the finger; release > threshold = close, otherwise spring back.

## Accessibility

- Lightbox is a modal — same focus trap rules — move focus into the dialog on open, keep Tab cycling inside it, and return focus to the thumbnail that launched it on close; mark it `role="dialog"` with `aria-modal="true"`.

- Each image has alt text from metadata — pull descriptive alt from captions or filenames, and show you have a graceful fallback when metadata is missing instead of shipping empty alt.

- Navigation buttons have explicit labels — give them `aria-label="Next image"` / `"Previous image"`, since an icon-only button reads as nothing to a screen reader.

- Keyboard navigation works (arrow keys, Escape) — every gesture needs a keyboard equivalent for next, previous, zoom, and close; a swipe-only lightbox is unusable without a pointer.

- Screen reader announces "image 3 of 47" on navigation — put the counter in an `aria-live="polite"` region so non-visual users hear their position update on each move.

## Performance

- Use modern formats (AVIF, WebP) with JPEG fallback — serve them through `<picture>` so the browser takes the smallest format it supports; AVIF often trims 30-50% of the bytes versus JPEG at similar quality.

- Serve responsive sizes via srcset — pair srcset with a `sizes` attribute so a phone downloads a thumbnail-sized file instead of the full desktop image.

- Decode images off-thread (decoding="async") — this keeps the decode step off the main thread so it doesn't stutter scrolling as thumbnails come into view.

- Use Image element width/height for aspect-ratio reservation — set intrinsic width and height (or `aspect-ratio`) so the grid reserves space and doesn't reflow as images load, a direct cumulative layout shift win.

## EXIF metadata

For photo galleries, show metadata: camera, lens, aperture, shutter, ISO. Parse client-side from JPEG headers using a library (exif-js, ExifReader).

## Common mistakes

- No virtualization with thousands of thumbnails — mounting several thousand `<img>` nodes drains memory and stalls scroll; interviewers expect you to window the DOM to what's visible.

- Lightbox covers the whole page but does not trap focus — Tab leaks to the page behind the overlay, the single most common accessibility miss in this question.

- Swipe gestures conflict with browser scroll — set `touch-action` so horizontal swipe and vertical scroll don't fight; get it wrong and both feel broken at once.

- Pinch-to-zoom that does not work reliably on real devices — simulators handle multi-touch differently than hardware, so test the pinch on an actual phone before you claim it works.

- Image decode blocks scrolling — a large synchronous decode freezes the main thread; `decoding="async"` plus right-sized sources keeps scroll smooth.

## Frequently Asked Questions

### Should I use a library or build from scratch?

For interview practice, build it. For production, use a library: PhotoSwipe, lightGallery, react-photo-gallery, swiper. They handle gesture and accessibility edge cases.

### How do I handle 4K images on mobile?

Serve appropriate resolution per device pixel ratio. iPhone Pro is 3x; serve 3x images. Don't serve 4K to a 1x display.

### What about video in the gallery?

Same architecture; video element instead of img. Same lazy-load and lightbox patterns. Add play/pause UI.
