Guides · August 20, 2023
Product Galleries, Zoom & Lightboxes in React
Building a great React product gallery means solving three problems well: fast image loading, a zoom interaction that feels native, and a lightbox that never traps keyboard or screen reader users. Here is a complete, dependency-light approach.
By Polo Themes
A React product gallery needs three things to feel professional: images that load fast and never shift the layout, a zoom interaction that reveals real product detail without fighting the user's cursor or fingers, and a lightbox that fully supports keyboard and screen reader navigation. Most teams get one of these right and quietly ship the other two broken. This tutorial builds a complete gallery from primitives, no black-box carousel library required, and calls out the accessibility and performance traps that show up once real product photography and real customers hit the page.
Product galleries look simple because the finished version is simple: a big image, some thumbnails, maybe a zoom on hover, and a lightbox for a closer look. What is not simple is getting there without introducing layout shift, without breaking on touch devices, and without shutting out keyboard and screen reader users along the way. This guide walks through a from-scratch build in React, then goes deeper on the two places teams get bitten hardest, hover and pinch zoom math and lightbox focus management, before covering performance and testing.
What a Product Gallery Actually Has to Do
Before writing a line of code, it is worth being precise about the job. A product gallery is not a generic image carousel with product photos dropped in, because the intent is different. A shopper looking at a carousel of blog thumbnails is browsing. A shopper looking at a product gallery is inspecting: they want to check stitching, verify a color under a specific light, confirm a logo placement, or make sure a mark in a photo is a reflection and not damage. That means the gallery has to support three distinct behaviors, each with its own failure modes.
Thumbnail navigation without layout shift
Swapping the main image on thumbnail click or hover sounds trivial, but if the incoming image has different natural dimensions than the outgoing one, the whole gallery, and everything below it, jumps. The fix is to reserve space with a fixed aspect-ratio container and fit the image inside it with object-fit, rather than letting the image element set the container's size. Do this once at the wrapper level and every subsequent image swap becomes a pure content change with zero layout recalculation.
A zoom that reveals detail instead of just enlarging pixels
Two very different interactions get called zoom. One is a hover magnifier on desktop, where moving the cursor over the image pans a larger version behind a lens or fills a side panel with a cropped, magnified region. The other is pinch-to-zoom or double-tap-to-zoom on touch, which needs its own gesture handling because browsers do not give you this for free on an arbitrary image element. A gallery that only implements one of these will feel broken on whichever device class it skipped.
A lightbox that behaves like a proper dialog
A lightbox is a modal, and modals have a well-understood accessibility contract: focus moves into the dialog when it opens, focus is trapped inside it while it is open, Escape closes it, and focus returns to the element that opened it on close. Skipping any one of these is the single most common accessibility failure on ecommerce product pages, because a lightbox that visually works with a mouse can still be completely unusable, or actively disorienting, for a keyboard or screen reader user.
Building the Base Gallery Component
Start with the data model. A gallery needs an ordered list of image variants, each with a source, a set of responsive sources for different viewport widths, width and height (so the browser can reserve space before the image decodes), and descriptive alt text. Resist the temptation to treat alt text as an afterthought here: for a product with multiple angles, generic alt text like "product photo 2" is a missed opportunity and a real accessibility gap. Alt text should describe what is different about that specific angle, for example a rear view showing a zipper pull or a close-up of a stitched seam.
The component itself splits cleanly into three pieces: a stage that renders the active image inside a fixed-aspect-ratio box, a thumbnail rail that lets the shopper jump to any image directly, and state that tracks which index is active. Keep that state as a single active index rather than duplicating it across the stage and the rail, and drive both from it. Arrow-key navigation on the stage and click or Enter on a thumbnail should both simply update that one piece of state.
For the stage image itself, load the first image eagerly and at high fetch priority since it is almost always the largest contentful paint element on a product page, and lazy-load every subsequent image so the browser is not fetching six or eight full-resolution photos before the shopper scrolls or clicks past the first one. If the framework offers a native image component with built-in responsive source generation, prefer it over a hand-rolled image tag; if it does not, generate your own responsive source set using the width descriptors your image pipeline already produces, and set explicit width and height attributes so the browser can allocate space before the file arrives.
Implementing Zoom: The Two Interactions That Actually Matter
For desktop hover zoom, the cleanest implementation avoids transform-based scaling of the visible image, which tends to look blurry past a modest zoom factor, and instead positions a much larger source image behind a fixed-size viewport using background-position math. On pointer move over the stage, compute the pointer's position as a percentage of the container's width and height, then set that same percentage as the background position of a zoomed layer sized well above 100 percent. The zoomed layer can be a sibling panel next to the image (a classic split-screen magnifier) or an overlay lens that follows the cursor; both use the same underlying math, just different presentation. The key detail people get wrong is using the low-resolution display image as the zoom source: always zoom into a separate, higher-resolution asset, or the "zoomed" view will just show larger, blurrier pixels and defeat the entire point of the feature.
For touch, do not try to repurpose the hover magnifier. Implement pinch-to-zoom and double-tap-to-zoom against a CSS transform on the image itself, tracking two-finger distance for pinch scale and a single tap-timing check for double-tap. Clamp the scale to a sensible minimum and maximum, and when the image is zoomed in, allow panning by translating it within the bounds of the enlarged image so the shopper never pans past the edge into empty space. Critically, disable the browser's own page-level pinch zoom and touch-action defaults only inside the gallery stage, not globally, so the rest of the page keeps its normal, expected touch behavior.
One detail that is easy to miss on both paths: zoom state should reset when the active image changes. A shopper zoomed in on the front view who then taps a thumbnail for the back view expects to see the back view at normal scale, not still zoomed to wherever the front view happened to leave the transform.
Building an Accessible Lightbox
The lightbox is where most galleries quietly fail an audit. Treat it as a proper dialog from the start rather than bolting accessibility on afterward. Concretely, that means: render it with a dialog role and an aria-modal attribute set to true, move focus to the first interactive element inside it the moment it opens, trap Tab and Shift+Tab so focus cycles only among the elements inside the lightbox, close it on Escape, and explicitly return focus to the thumbnail or image that triggered it when it closes. If the underlying UI library or design system already ships a dialog primitive, use it here rather than reimplementing focus trapping by hand; hand-rolled focus traps are a common source of subtle bugs, like focus escaping to the browser chrome when the last focusable element is a disabled button.
Lock body scroll while the lightbox is open, but do it in a way that does not shift the page horizontally when the scrollbar disappears, since that shift is jarring and, on some layouts, causes a visible layout jump right as the shopper's attention is on the enlarged image. Compensate by adding right padding to the body equal to the scrollbar width for the duration the lightbox is open, then remove it on close.
Inside the lightbox, keep the same arrow-key navigation the main gallery uses, and announce the current position to assistive technology, for example "image 3 of 7", using a visually hidden live region or an aria-label on the image container. This single addition is what separates a lightbox that is merely navigable from one that is genuinely usable without sight.
Finally, load the full-resolution image for the lightbox lazily, only once the lightbox actually opens, rather than preloading every full-resolution asset up front. A product with a dozen high-resolution photos should not cost a shopper who never opens the lightbox a dozen extra full-size downloads.
Performance: Where Galleries Quietly Cost You Core Web Vitals
The product gallery is usually the single heaviest element on a product detail page, and it is disproportionately responsible for a slow largest contentful paint and layout shift score. A few disciplines fix most of it. Serve responsive image sources so a phone on a narrow viewport is not downloading a desktop-sized asset. Reserve layout space with explicit dimensions or an aspect-ratio box for every image, including thumbnails, so nothing shifts as images decode in whatever order the network delivers them. Mark the first, above-the-fold stage image as high priority and everything else as lazy, and avoid animating properties that trigger layout or paint, such as width or top, in favor of transform and opacity, which the browser can composite on its own thread. Modern image formats with strong compression are worth serving where the pipeline supports them, with a fallback for older browsers, since gallery-heavy pages are exactly where format choice has the largest real-world payload impact.
If you are working from Figma before any of this gets built, it genuinely helps to design the gallery states, default, hover-zoom, touch-zoom, and open lightbox, as real artboards rather than leaving them as an implied interaction a developer has to guess at. Our Figma UI kits include fully laid-out product gallery and lightbox states for exactly this reason, so design and engineering are working from the same source of truth on something that is easy to under-specify.
Testing the Gallery Before You Ship It
A short, repeatable manual pass catches most of the failures described above before they reach customers.
- Unplug the mouse and navigate the entire gallery, thumbnails, zoom trigger, and lightbox open/close/next/previous, using only the keyboard. Every interactive element should be reachable and show a visible focus state.
- Open the lightbox and confirm focus lands inside it immediately, Tab cycles only within it, Escape closes it, and focus returns to the exact thumbnail or image that opened it.
- Turn on a screen reader and confirm alt text is meaningful per image, the lightbox announces itself as a dialog, and position, such as image 3 of 7, is announced on navigation.
- On an actual touch device, confirm pinch and double-tap zoom both work, panning stays within the image bounds, and the rest of the page still scrolls and pinch-zooms normally outside the gallery.
- Throttle the network to a slow connection and confirm the first stage image still paints quickly, thumbnails lazy-load sensibly, and nothing shifts position as images finish decoding.
- Switch the active image several times in a row and confirm zoom state resets each time rather than carrying over from the previous image.
Frequently Asked Questions
Should I build this myself or use a library?
If your stack already includes a well-maintained, actively updated gallery or lightbox library that meets the dialog and focus-trap requirements above, using it is entirely reasonable and often faster. The risk with pre-built libraries is usually accessibility debt, an unmaintained dependency, or a bundle size that is heavier than a purpose-built component would be. Read the library's own accessibility documentation and test it with a keyboard and a screen reader before trusting it, the same way you would test a hand-rolled component.
Do I need separate hover-zoom and pinch-zoom implementations?
Yes, in almost every case. Hover zoom depends on a precise pointer position that touch devices do not reliably provide, and pinch-to-zoom depends on multi-touch gesture tracking that desktop pointer events do not provide. Detect the input type and branch the interaction rather than trying to force one implementation to cover both.
What is the single most common gallery bug you see in production?
Layout shift from image swaps that were never given a fixed aspect ratio, closely followed by lightboxes that trap or lose keyboard focus. Both are cheap to prevent up front and expensive to retrofit once a page has shipped and real content has revealed every edge case a design mockup did not.
Does this approach work the same way in a Next.js app?
The interaction patterns, aspect-ratio boxes, hover-zoom math, dialog-based lightbox, are framework agnostic and apply whether the gallery lives in a client component or is rendered on the server and hydrated. The main Next.js-specific decision is whether to use the framework's built-in image component, which already handles responsive sources and lazy loading, versus a manual image element; for most product pages, reaching for the built-in component and layering the zoom and lightbox interactions on top of it is the more maintainable path.