Product gallery React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React product gallery for detail pages: a swipeable photo stage that zooms under the mouse, a thumbnail rail and a full-screen viewer.
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.
Product gallery is a Pro React component in the React commerce components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add product-gallery, together with Button, Carousel, Image Lightbox, useControllableState, useMediaQuery and 2 more and the npm packages lucide-react and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add product-galleryProduct gallery is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import ProductGallery from @/components/ui/product-gallery.
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
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.
defaultIndexdefault0numberThe photo on the stage at first. Out of range values clamp to the first or last photo.
indexnumberControlled photo index. The stage, the thumbnails and the viewer all follow it.
onIndexChange(index: number) => voidCalled when a swipe, an arrow, a key, a thumbnail or the viewer changes the photo.
defaultOpendefaultfalsebooleanStarts with the full-screen viewer open at the current photo.
openbooleanControlled viewer state.
onOpenChange(open: boolean) => voidCalled when the viewer opens (a click, a tap, Enter, the expand button, +N) or closes.
thumbnailsdefault"start""start" | "bottom" | "none"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.
aspectdefault"4/3""1/1" | "4/3" | "3/4" | "16/10"The stage's shape. Photos are cropped (cover) or letterboxed (contain) into it.
fitdefault"cover""cover" | "contain"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.
zoomdefault"hover""hover" | "lens" | "none"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.
zoomScaledefault2numberHow far the hover zoom or the lens magnifies.
lightboxdefaulttruebooleanA click, a tap or Enter opens the full-screen ImageLightbox at that photo, growing out of the stage. Off: no viewer, no expand button.
indicatordefault"counter""counter" | "dots" | "none"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.
showArrowsdefaulttruebooleanPrevious / next buttons over the stage for mouse and trackpad users, shown on hover or keyboard focus; touch screens swipe instead.
loopdefaultfalsebooleanWraps from the last photo to the first, on the stage and in the viewer.
maxThumbnailsdefault6numberHow many thumbnails show. When there are more photos the last visible one reads "+N" and opens the viewer at that photo.
radiusdefault"md""none" | "sm" | "md" | "lg"The stage's corners: 0 / 12 / 16 / 24px. Thumbnails follow one step smaller.
bleedOnMobiledefaultfalsebooleanBelow 768px the stage drops its corners, for detail pages that run the gallery edge to edge.
badgesReactNodeTop start, over the photo: a "Promovat" badge, the condition.
actionsReactNodeTop end, over the photo: e.g. <FavoriteButton variant="overlay" /> and a share button (keep them 44px on touch).
lightboxPropsPartial<ImageLightboxProps>Anything else for the viewer (labels, download, share, showThumbnails, maxZoom...). images, open and index belong to the gallery.
labelsPartial<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".
classNamesPartial<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
constantThe 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
typeOne 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.
Dependencies
Files
- components/ui/product-gallery.tsx
FAQ
Is the React Product gallery component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Product gallery?
Run npx wingo-ui@latest add product-gallery. It also adds the Wingo UI items it builds on (Button, Carousel, Image Lightbox, useControllableState, useMediaQuery and 2 more) and the npm packages lucide-react and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/product-gallery.
How does Product gallery behave on phones?
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.
See also
Used in
More React commerce components
Articles using this component
- Shadcn Ecommerce Template: Five Screens That Work on Phones
Build it: screens and apps11 min read
- React Image Lightbox: Zoom, Swipe and Pull to Close
Component guides9 min read





