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.
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"witharia-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
sizesattribute 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-actionso 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.
Useful next steps:
