Build an Image Gallery with Lightbox

Updated · techinterview.org

An image gallery with lightbox (thumbnails grid + tap-to-zoom modal) appears in many products. The interview 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.

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.

  • 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.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

newsletter

What's actually being asked right now

Interview patterns & comp trends, straight to your inbox.

No spam. Unsubscribe anytime.

1972 Soviet postage stamp commemorating the Mars 2 probe

worth a read

Mars For The Rest of Us — a weekly-or-more deep dive on the technical side of Mars exploration: rocket propulsion, microbiology, mission architecture, and everything in between. Written by Maciej Ceglowski.

Read it on Substack
Scroll to Top