# React Image Lightbox: Zoom, Swipe and Pull to Close

> Build a React image lightbox that grows out of its thumbnail, swipes between photos, pinch zooms and closes with a swipe down on phones, with working code.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 19, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 9 min
- Canonical: https://wingo-ui.com/blog/react-image-lightbox

## TL;DR

A good React image lightbox opens out of the thumbnail you tapped, swipes one photo at a time, zooms with a pinch or a double tap, and closes with a swipe down on phones, then hands focus back to the thumbnail. The Wingo UI Image Lightbox does this from an images array, and LightboxGallery adds the thumbnail grid. For product and listing pages, the Product Gallery puts a hover zoom stage and a thumbnail rail in front of the same viewer.

You have a grid of photos and want a React image lightbox: tap a photo, see it full screen, swipe to the next one, zoom in on a detail. The happy path is quick. Then the photo pops in from nowhere instead of growing out of the tile, the frame jumps when the file arrives, a pinch zooms the whole page, a swipe down does nothing on a phone, and after closing, keyboard focus lands at the top of the document. This guide covers what a production lightbox has to handle, the props that matter in the Wingo UI [Image Lightbox](https://wingo-ui.com/components/image-lightbox), and when the [Product Gallery](https://wingo-ui.com/components/product-gallery) is the better fit.

## What should a React image lightbox do?

Open from the thumbnail, page one photo per swipe, zoom where the user points, close the way a phone's photo app closes, and keep keyboard and screen reader users oriented. Here is the list we hold our own viewer to:

1. **Grow out of the thumbnail** on open, and fly back into the current photo's tile on close.
2. **Swipe between photos**, one per swipe, with arrows and a thumbnail strip on desktop.
3. **Zoom** with a pinch, a double tap or a trackpad pinch, and pan with momentum.
4. **Close with a swipe down** on touch screens.
5. **No layout shift** while images load, and no loading of every full size file up front.
6. **Keyboard and focus**: arrows, Escape, a focus trap, and focus returned to the thumbnail of the photo on screen when the viewer closes.

## Which React lightbox libraries are still maintained?

Yet Another React Lightbox still ships: version 3.32.2 went to npm in July 2026. PhotoSwipe's latest release is 5.4.4 from May 2024, and its README says v6 is in development. The package literally named `react-image-lightbox` was archived on January 19, 2023, and its README points to `react-photoswipe-gallery` instead. Facts below come from each project's docs, README and npm page, as of October 2026.

| Option | What it is | License | Zoom and thumbnails | Pull to close |
|---|---|---|---|---|
| [react-image-lightbox](https://github.com/frontend-collective/react-image-lightbox) | Archived package, read only since January 2023 | MIT | Built in zoom; thumbnails only as loading placeholders | Not documented |
| [Yet Another React Lightbox](https://yet-another-react-lightbox.com/) | React package, works with React 16.8 through 19 | MIT | Zoom and Thumbnails are optional plugins | `closeOnPullDown` and `closeOnPullUp` in the controller settings, both `false` by default |
| [PhotoSwipe](https://photoswipe.com/) | Framework agnostic library (v5.4.4), with React, Vue and Svelte guides | MIT | Built in zoom; you must pass each image's width and height | `closeOnVerticalDrag` and `pinchToClose`, both `true` by default |
| [Wingo UI Image Lightbox](https://wingo-ui.com/components/image-lightbox) | Pro source component you copy into your project | Commercial | Built in zoom, pan and a thumbnail strip | `swipeToClose`, on by default for touch and pen |

PhotoSwipe is a good pick if you want a mature, framework agnostic viewer and you are fine wiring it into React yourself; its docs have a dedicated page on animating from a cropped thumbnail. Yet Another React Lightbox fits if you want a React package with zoom and thumbnails as opt-in plugins. If you searched for a shadcn lightbox, note that shadcn/ui has none: it ships a [Carousel built on Embla](https://ui.shadcn.com/docs/components/carousel) and a Dialog, and you would combine them yourself. The Wingo UI viewer is that combination already built: an Embla based [Carousel](https://wingo-ui.com/components/carousel) inside a Radix dialog primitive (the same one our [Dialog](https://wingo-ui.com/components/dialog) uses), with the gestures on top.

## What props does the Image Lightbox need?

One: `images`, an array of `{ src, alt, caption?, width?, height?, srcSet?, sizes?, thumbnail?, downloadName? }`. The quickest way to a React lightbox gallery is `LightboxGallery`, which renders a grid of thumbnail buttons and wires each tile up as the origin its photo flies out of.

```bash
npx wingo-ui@latest add image-lightbox
```

```tsx
"use client";

import { LightboxGallery, type LightboxImage } from "@/components/ui/image-lightbox";

const photos: LightboxImage[] = [
  { src: "/photos/living.jpg", alt: "Bright living room", caption: "Bright living room, 260 sq ft", width: 960, height: 600 },
  { src: "/photos/sofa.jpg", alt: "Green velvet sofa", caption: "Velvet sofa, stays with the apartment", width: 960, height: 600 },
  { src: "/photos/desk.jpg", alt: "Desk corner by the window", caption: "Desk corner by the window", width: 960, height: 600 },
  { src: "/photos/parking.jpg", alt: "Underground parking spot", caption: "Parking spot included in the rent", width: 960, height: 600 },
];

export function ListingPhotos() {
  return <LightboxGallery images={photos} columns={{ base: 2, md: 4 }} featured maxVisible={5} />;
}
```

`featured` makes the first tile span 2 by 2, and `maxVisible={5}` turns the fifth tile into "+N" when there are more photos. The grid defaults to 2 columns, 3 from 640px and 4 from 768px, with square tiles and 12px corners; `aspect`, `gap` and `radius` change that.

Give every image its `width` and `height` so the frame takes the photo's shape before the file loads, and a small `thumbnail` file for the grid, the strip and the loading placeholder. Only the current photo and its neighbors load, and a spinner sits over the placeholder until the file is ready.

The props you will reach for next are `zoom` (on by default), `maxZoom` (4), `doubleTapZoom` (2.5), `loop` (off), `download` and `share` (both off), and `hideControlsDelay`, which fades the desktop toolbar after 3000ms without a mouse move. Every string lives in `labels`, merged over `DEFAULT_IMAGE_LIGHTBOX_LABELS`.

> Live demo (Image Lightbox): Tap a photo and it grows out of its tile. Swipe or use the arrow keys to page, double-click to zoom into a spot, then press Escape twice: once to reset the zoom, once to close. Try it at [Image Lightbox](https://wingo-ui.com/components/image-lightbox) and install it with `npx wingo-ui@latest add image-lightbox`.

## How does the lightbox zoom out of its thumbnail?

The viewer measures the element the photo came from, starts the photo at that rectangle with that crop and corner radius, and animates it to the fitted size with a spring. `LightboxGallery` does the wiring for you. For your own layout, such as one big cover photo with an outside thumbnail strip, use `ImageLightbox` directly with a `trigger` and a `getOrigin` that returns the element for each index:

```tsx
"use client";

import { useRef, useState } from "react";
import { ImageLightbox, type LightboxImage } from "@/components/ui/image-lightbox";

export function CoverPhoto({ photos }: { photos: LightboxImage[] }) {
  const [index, setIndex] = useState(0);
  const cover = useRef<HTMLButtonElement>(null);

  return (
    <ImageLightbox
      images={photos}
      index={index}
      onIndexChange={setIndex}
      // close back into the cover, whichever photo is showing
      getOrigin={() => cover.current}
      trigger={
        <button ref={cover} type="button" aria-label={`Open ${photos[index].alt}`} className="block w-full overflow-hidden rounded-2xl">
          <img src={photos[index].src} alt="" className="aspect-[16/10] w-full object-cover" />
        </button>
      }
    />
  );
}
```

Because `index` is controlled, the cover follows the photo the user swiped to, so the viewer closes into the right image. Inside a `LightboxGallery` with custom children, the `useLightbox` hook gives you `open(index, origin)`: call `open(2, event.currentTarget)` from any button and photo 3 zooms out of it, with focus returning there on close.

Under reduced motion the viewer fades instead of flying (see the [prefers-reduced-motion guide](https://wingo-ui.com/blog/prefers-reduced-motion-react)).

## How do swipe, pinch zoom and swipe down to close work on phones?

On a phone the viewer is full bleed, black with white controls in both themes, and clears the notch and home indicator with `env(safe-area-inset-*)`. A tap toggles the controls.

- **Swipe sideways** to page, with Embla's physics.
- **Pinch** to zoom around your fingers. It resists past 1x and past `maxZoom`, then settles. A double tap zooms to `doubleTapZoom` at the tapped point, and a second double tap goes back.
- **While zoomed**, a swipe pans the photo instead of paging, and swipe down is off. Changing photo resets the zoom.
- **Pull down** on an unzoomed photo and it shrinks toward 0.85 while the black fades and the page shows through. Let go past a quarter of the screen height, or flick faster than 400px per second, and it closes; otherwise it springs back.

People expect pull to close after years of native photo apps, so the close button is the fallback here and the gesture is the main way out. Set `swipeToClose={false}` when the viewer sits on top of something that already uses vertical drags. The [React bottom sheet guide](https://wingo-ui.com/blog/react-bottom-sheet-for-the-web) covers the same drag to dismiss pattern for panels.

## How do I make an image lightbox accessible?

Treat it as a modal dialog. The viewer is built on the Radix dialog primitive, so focus is trapped inside while it is open. On close, focus goes to the tile of the photo on screen, or back to whatever opened the viewer when that tile is not focusable. The hidden dialog title defaults to "Image viewer", the current caption describes the dialog, and the "3 / 8" counter is a polite live region, so a screen reader hears each page turn.

Keys: Left and Right for previous and next, Home and End for the first and last photo, `+` or `=` to zoom in, `-` to zoom out, `0` to reset. Escape resets a zoom first and closes on the second press, so a user who zoomed in by accident does not lose their place. Each `LightboxGallery` tile is a real button named from `labels.openImage` and the image's `alt`, so write real alt text.

## When should I use the Product Gallery instead?

Use it on a product or listing detail page, where the photos are the page's main content. The Product Gallery is a product gallery React component with a swipeable stage, a thumbnail rail and the Image Lightbox behind it:

```tsx
"use client";

import { ProductGallery, type ProductGalleryImage } from "@/components/ui/product-gallery";

const images: ProductGalleryImage[] = [
  { src: "/products/chair-front.jpg", alt: "Oak lounge chair, front", width: 1200, height: 900 },
  { src: "/products/chair-side.jpg", alt: "Oak lounge chair, side", width: 1200, height: 900 },
  { src: "/products/chair-detail.jpg", alt: "Wool upholstery detail", width: 1200, height: 900 },
];

export function ProductPhotos() {
  return <ProductGallery images={images} fit="contain" zoom="lens" bleedOnMobile lightboxProps={{ download: true }} />;
}
```

With a mouse, `zoom="hover"` (the default) scales the photo in place and pans with the cursor, and `zoom="lens"` shows a round magnifier instead. Either way the zoom never goes past twice the photo's natural width, so small files do not turn to mush. `fit="contain"` letterboxes product shots on white in light mode. Thumbnails sit in a rail beside the stage once the gallery is at least 512px wide (a container query, so it works in a narrow column too), and the last visible one reads "+N".

On a phone there is no hover zoom and no thumbnail rail: the stage swipes, a "3 / 12" counter or dots show the position, and a tap opens the full-screen viewer where pinch zoom and swipe down to close work as above. That split is the honest answer to "react image gallery zoom": hover zoom is for mice, pinch is for fingers, and one component should not pretend otherwise.

> Live demo (Product gallery): Rest the mouse on the photo to see the lens follow the cursor, then click to open the full-screen viewer. On a phone, swipe the stage and tap to open. Try it at [Product gallery](https://wingo-ui.com/components/product-gallery) and install it with `npx wingo-ui@latest add product-gallery`.

## What are the common React lightbox pitfalls?

Three more show up once real photos replace the demo set. Missing dimensions is covered above; the fix is to store width and height when the photo is uploaded.

- **Full size files in the grid.** Pass a `thumbnail` per image, or a `srcSet` and `sizes`.
- **Swipe down on a zoomed photo.** In your own viewer, turn the close gesture off while the scale is above 1, or a pan closes it.
- **A broken file that blocks paging.** A missing image in the Wingo UI viewer shows an icon in the tile and in the viewer, and swiping past it still works. Test that case with your own data.

If users upload these photos, the [React image cropper guide](https://wingo-ui.com/blog/react-image-cropper) covers fixing the aspect ratio before they reach the gallery.

## How do I add it to my project?

Run `npx wingo-ui@latest add image-lightbox` for the viewer and grid, or `npx wingo-ui@latest add product-gallery` for the detail page gallery (it pulls in the lightbox). The CLI copies the component into `components/ui`, adds the carousel, hooks and helpers it imports, and installs its npm packages. Both are Pro components ([pricing](https://wingo-ui.com/pricing)). More single component deep dives are in the [component guides category](https://wingo-ui.com/blog/category/components), and the [React form components guide](https://wingo-ui.com/blog/react-form-components-guide) applies the same touch, keyboard and screen reader rules to every form control.

## Components in this post

- [Image Lightbox](https://wingo-ui.com/components/image-lightbox) (Pro): A React image lightbox: a full-screen photo viewer that zooms from its thumbnail, with swipe, pinch zoom and swipe down to close. Install: `npx wingo-ui@latest add image-lightbox`
- [Product gallery](https://wingo-ui.com/components/product-gallery) (Pro): A React product gallery for detail pages: a swipeable photo stage that zooms under the mouse, a thumbnail rail and a full-screen viewer. Install: `npx wingo-ui@latest add product-gallery`

## FAQ

### What is the best React image lightbox library?

For a packaged library, Yet Another React Lightbox (MIT, React 16.8 to 19, zoom and thumbnails as plugins) and PhotoSwipe (MIT, framework agnostic, with a React guide) are the usual choices. The original react-image-lightbox package was archived in January 2023. The Wingo UI Image Lightbox is a Pro source component you copy into your project.

### Is there a shadcn lightbox component?

No. shadcn/ui ships a Carousel built on Embla and a Dialog, but no lightbox or image viewer as of October 2026. The Wingo UI Image Lightbox combines the same two ideas (an Embla carousel inside a Radix dialog) and installs with npx wingo-ui@latest add image-lightbox.

### How do I close a lightbox with a swipe down on mobile?

Track the vertical drag on the photo, scale the photo down and fade the backdrop as it follows the finger, and close when the drag passes a set share of the screen or a fast flick. The Wingo UI viewer closes past a quarter of the screen height or a flick faster than 400px per second, and springs back otherwise.

### Why does my lightbox jump when the image loads?

The viewer does not know the photo's shape until the file arrives. Pass width and height for each image so the frame takes its final aspect ratio at once, and pass a small thumbnail file to show as a placeholder while the full one loads.

### Is the Wingo UI Image Lightbox free?

No. The Image Lightbox, the Product Gallery and the Carousel they build on are Pro components. The Button and the Spinner they use are free.

---

Source: https://wingo-ui.com/blog/react-image-lightbox
