# Product gallery

> A React product gallery for detail pages: a swipeable photo stage that zooms under the mouse, a thumbnail rail and a full-screen viewer.

- Type: React component (Commerce)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add product-gallery`
- Docs: https://wingo-ui.com/components/product-gallery

## Interaction

With a mouse, rest on the photo and it grows to the zoom scale on a smooth spring, panning with the cursor so the spot under it stays put; the lens mode shows a round magnifier that follows the cursor instead (it stays over the photo, never the letterbox bars). Leave and it settles back; start dragging the photo sideways and the zoom lets go so the carousel can move. Arrows fade in on hover or focus, the expand button sits bottom right and a hint chip says what to do. Click the photo (zoomed or not) or the expand button and the viewer opens, the photo growing out of the stage; swipe or page there and the stage follows, so the viewer closes back into the photo you are on. Thumbnails sit in a rail beside the stage or in a strip below it (a narrow column with a mouse gets the strip), the current one ringed; the last visible one reads +N and opens the viewer there. On a phone the stage is full width: swipe between photos (vertical scrolling always wins), read the 3 / 12 counter or the dots, tap to open the viewer and pinch there; there is no hover zoom and no thumbnail rail, and the favorite and share buttons over the photo keep 44px targets. Keys: Tab to the stage, Left / Right change the photo, Home / End jump to the first / last, Enter or Space open the viewer and focus comes back to the stage when it closes; the thumbnails are one Tab stop where the arrow keys and Home / End move focus and Enter or Space show that photo. Under reduced motion the zoom follows the cursor without the spring and the viewer fades.

## Installation

```bash
npx wingo-ui@latest add product-gallery
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/product-gallery` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/product-gallery.tsx`

npm dependencies: `npm i lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Carousel](https://wingo-ui.com/components/carousel) ([Markdown](https://wingo-ui.com/components/carousel.md))
- [Image Lightbox](https://wingo-ui.com/components/image-lightbox) ([Markdown](https://wingo-ui.com/components/image-lightbox.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { ProductGallery, type ProductGalleryImage } from "@/components/ui/product-gallery"
import { FavoriteButton } from "@/components/ui/favorite-button"
import { Badge } from "@/components/ui/badge"
import { Sparkles } from "lucide-react"

const images: ProductGalleryImage[] = [
  { src: "/demo/apartment.jpg", width: 960, height: 600, alt: "Bright living room", caption: "Bright living room, 24 m²" },
  { src: "/demo/sofa.jpg", width: 960, height: 600, alt: "Green velvet sofa", caption: "The lounge corner" },
  { src: "/demo/city.jpg", width: 960, height: 600, alt: "View over the boulevard", caption: "View from the balcony" },
]

export function ListingPhotos() {
  return (
    <ProductGallery
      images={images}
      bleedOnMobile
      badges={<Badge variant="soft" tone="warning" leftIcon={<Sparkles />}>Promovat</Badge>}
      actions={<FavoriteButton variant="overlay" />}
      labels={{ gallery: "Listing photos", open: "Open full screen", noImage: "No photos yet" }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `images` | `ProductGalleryImage[]` |  | The photos: { src, alt, caption?, width?, height?, srcSet?, sizes?, thumbnail? } (LightboxImage plus a thumbnail file). width and height let the viewer take its final shape before the file loads; captions show in the viewer. |
| `defaultIndex?` | `number` | `0` | The photo on the stage at first. Out of range values clamp to the first or last photo. |
| `index?` | `number` |  | Controlled photo index. The stage, the thumbnails and the viewer all follow it. |
| `onIndexChange?` | `(index: number) => void` |  | Called when a swipe, an arrow, a key, a thumbnail or the viewer changes the photo. |
| `defaultOpen?` | `boolean` | `false` | Starts with the full-screen viewer open at the current photo. |
| `open?` | `boolean` |  | Controlled viewer state. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when the viewer opens (a click, a tap, Enter, the expand button, +N) or closes. |
| `thumbnails?` | `"start" \| "bottom" \| "none"` | `"start"` | start: a vertical rail beside the stage, exactly as tall as it (it scrolls when the photos do not fit), once the gallery is at least 512px wide (a container query); in a narrower column a mouse gets it as a strip under the stage. bottom: a strip under the stage. On touch screens below 512px the indicator replaces both. |
| `aspect?` | `"1/1" \| "4/3" \| "3/4" \| "16/10"` | `"4/3"` | The stage's shape. Photos are cropped (cover) or letterboxed (contain) into it. |
| `fit?` | `"cover" \| "contain"` | `"cover"` | cover fills the stage and crops; contain shows the whole photo on white in light mode and on the tinted stage in dark mode, for product shots. |
| `zoom?` | `"hover" \| "lens" \| "none"` | `"hover"` | Mouse pointers only. hover scales the photo in place and pans with the cursor; lens shows a round magnifier. The zoom never goes past twice a photo's natural width, and touch screens use the viewer's pinch instead. |
| `zoomScale?` | `number` | `2` | How far the hover zoom or the lens magnifies. |
| `lightbox?` | `boolean` | `true` | A click, a tap or Enter opens the full-screen ImageLightbox at that photo, growing out of the stage. Off: no viewer, no expand button. |
| `indicator?` | `"counter" \| "dots" \| "none"` | `"counter"` | The position cue where thumbnails do not show (touch screens below 512px of gallery width, or thumbnails="none"): a "3 / 12" chip at the bottom end or dots at the bottom center. |
| `showArrows?` | `boolean` | `true` | Previous / next buttons over the stage for mouse and trackpad users, shown on hover or keyboard focus; touch screens swipe instead. |
| `loop?` | `boolean` | `false` | Wraps from the last photo to the first, on the stage and in the viewer. |
| `maxThumbnails?` | `number` | `6` | How many thumbnails show. When there are more photos the last visible one reads "+N" and opens the viewer at that photo. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg"` | `"md"` | The stage's corners: 0 / 12 / 16 / 24px. Thumbnails follow one step smaller. |
| `bleedOnMobile?` | `boolean` | `false` | Below 768px the stage drops its corners, for detail pages that run the gallery edge to edge. |
| `badges?` | `ReactNode` |  | Top start, over the photo: a "Promovat" badge, the condition. |
| `actions?` | `ReactNode` |  | Top end, over the photo: e.g. <FavoriteButton variant="overlay" /> and a share button (keep them 44px on touch). |
| `lightboxProps?` | `Partial<ImageLightboxProps>` |  | Anything else for the viewer (labels, download, share, showThumbnails, maxZoom...). images, open and index belong to the gallery. |
| `labels?` | `Partial<ProductGalleryLabels>` |  | gallery "Photos", thumbnails "Photo thumbnails", open "Open full screen", thumbnail (i, n) => "Show photo i of n", more (n) => "+n", counter (i, n) => "i / n", zoomHint "Hover to zoom, click to open", noImage "No photos", previous "Previous photo", next "Next photo", slide (i, n) => "i of n". |
| `classNames?` | `Partial<Record<"thumbnails" \| "thumbnail" \| "stage" \| "slide" \| "image" \| "lens" \| "counter" \| "dots" \| "hint" \| "expand" \| "badges" \| "actions", string>>` |  | Extra classes for the inner parts. |

## Parts

### DEFAULT_PRODUCT_GALLERY_LABELS (constant)

The English copy: gallery "Photos", open "Open full screen", the thumbnail and counter text, zoomHint and noImage. Spread it into labels to translate a few keys.

### ProductGalleryImage (type)

One photo, the same shape as an image-lightbox image: src, alt, caption, width, height, srcSet, sizes and an optional thumbnail url for the rail and the placeholder.

## Examples

- **Product on white**: fit="contain" on a square stage with a bottom strip: studio shots of different ratios are letterboxed on white.
- **Lens zoom**: A round magnifier follows the mouse at 3x; touch screens skip it and open the viewer instead.
- **Phone detail page**: bleedOnMobile with the counter chip and no thumbnails, the way a listing page opens on a phone.
- **Many photos**: Ten photos with maxThumbnails={5}: the last thumbnail reads +5 and opens the viewer there.
- **Controlled index**: Buttons outside the gallery jump to the living room, the balcony or the floor plan.
- **One photo, a broken file, none**: A single photo hides every control, a broken file shows an icon tile, an empty list the no-photos tile.

## See also

- [Image Lightbox](https://wingo-ui.com/components/image-lightbox) ([Markdown](https://wingo-ui.com/components/image-lightbox.md))
- [Carousel](https://wingo-ui.com/components/carousel) ([Markdown](https://wingo-ui.com/components/carousel.md))
- [Listing card](https://wingo-ui.com/components/listing-card) ([Markdown](https://wingo-ui.com/components/listing-card.md))
- [Seller card](https://wingo-ui.com/components/seller-card) ([Markdown](https://wingo-ui.com/components/seller-card.md))
- [Favorite button](https://wingo-ui.com/components/favorite-button) ([Markdown](https://wingo-ui.com/components/favorite-button.md))

---

Canonical: https://wingo-ui.com/components/product-gallery
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
