Product galleryPro
A product gallery with a hover magnifier, gliding thumbnails, and color and size variants that crossfade photos.
Live · keyboard ready
Washed linen duvet set
0$248From $188 · queen shown
Stonewashed European flax that softens with every wash. Hover the photo to look closer, or open it fullscreen.
ColorOat
SizeSelect a size
- Product detail pages with several photos per color and a size picker.
- Stores where shoppers need to zoom into fabric, texture, or small print.
- Variants that sell out per color and should be struck through, not hidden.
- Use lightbox-gallery for a photo grid that opens into a viewer, with no variants.
- Use carousel for marketing slides or cards.
- Use photo-grid for a masonry of images without a main photo.
Installation
Pro source and install commands unlock with a Pro plan.
example.tsx
import { useState } from "react";import { ProductGallery } from "@/registry/components/product-gallery/product-gallery";import Button from "@/registry/components/button/button"; export function ProductPage() { const [size, setSize] = useState<string | null>(null); return ( <ProductGallery label="Field jacket" colors={[ { value: "olive", label: "Olive", swatch: "#6b6a4a", images: [{ src: "/jacket/olive-1.jpg", alt: "Olive jacket, front" }, { src: "/jacket/olive-2.jpg", alt: "Olive jacket, back" }] }, { value: "navy", label: "Navy", swatch: "#27304a", images: [{ src: "/jacket/navy-1.jpg", alt: "Navy jacket, front" }] }, ]} sizes={[ { value: "s", label: "S" }, { value: "m", label: "M", soldOut: ["navy"] }, { value: "l", label: "L" }, ]} size={size} onSizeChange={setSize} header={<><h1>Field jacket</h1><p>$240</p></>} footer={<Button disabled={!size}>Add to bag</Button>} /> );}API reference
2 parts. The first is the root.
ProductGallery
A product media gallery with a hover magnifier, a thumbnail rail, swipe on touch, a fullscreen zoom viewer, and a color and size picker.
PropTypeDefaultDescription
colorsRequiredProductGalleryColor[]–Colors: { value, label, swatch, images, soldOut? }. Each carries its own images: { src, alt, srcSet? }.sizesProductGallerySize[]–Sizes: { value, label, soldOut? }. soldOut is true for every color or a list of color values.colorstring–Controlled color value.defaultColorstring–Starting color when uncontrolled. Defaults to the first color.onColorChange(color: string) => void–Called when a swatch is chosen.sizestring | null–Controlled size value, or null for none.defaultSizestring | nullnullStarting size when uncontrolled.onSizeChange(size: string | null) => void–Called when a size is chosen or cleared, including when a color change sells it out.indexnumber–Controlled index of the photo on show.defaultIndexnumber0Starting photo when uncontrolled.onIndexChange(index: number) => void–Called when the photo changes.zoomnumber2.4Magnification of the hover lens and the fullscreen zoom.magnifierbooleantrueTurns the hover lens on or off.aspectRationumber4 / 5Aspect ratio of the main photo and thumbnails, as width / height.labelstring"Product photos"Product name, used to name the gallery and its viewer.headerReactNode–Content above the picker, such as the name and price.footerReactNode–Content under the picker, such as add to bag.classNamestring–Class on the root.refRef<HTMLDivElement>–The root element.isSoldOut
Whether a size is sold out in a given color.
PropTypeDefaultDescription
sizeRequiredProductGallerySize–The size entry.colorRequiredstring–The color value.- ArrowLeftorArrowRight
- Steps photos on the main photo, the thumbnail rail, and in the viewer.
- Arrow keysorHomeorEnd
- Move and select within the thumbnail rail, swatches, and sizes; sold-out sizes are skipped.
- EnterorSpace
- Opens the fullscreen viewer from the main photo.
- Escape
- Zooms out in the viewer, then closes it and returns focus to the photo.
- Tab
- Cycles within the viewer's buttons while it is open.
- The main photo is a button labelled with the photo's alt text, its position, and Open fullscreen.
- Thumbnails are a tablist; swatches and sizes are radiogroups with roving tabindex, and sold-out sizes use aria-disabled.
- The viewer is a modal dialog that focuses Close on open, traps Tab, locks body scroll, and announces the photo count politely.
- Every image needs a meaningful alt; thumbnails and the lens are decorative copies.
- The photo track springs to the photo on show and follows a finger one to one, with rubber-band edges; one flick moves one photo.
- The lens rides a spring behind the pointer; rail, swatch, and size highlights glide to the active option.
- The viewer grows out of the photo's own rectangle with counter-scaled corners and returns to it on close; zoom scales on a spring and pans after the pointer.
- Changing color crossfades every photo from a blur. Reduced motion jumps the track, lens, and zoom, and replaces the viewer flight with a fade.
- The root is a container: photos and details sit side by side above 680px, stack at 680px and below, and the rail moves under the photo at 520px and below.
- On touch screens (hover: none) the photo shows dots and a permanent expand icon; the lens is mouse only.
- The viewer frame sizes to fit the viewport at the photo's ratio, with 12px padding under 640px and 48px above; it refits on resize.
- Only the photo on show loads eagerly; the rest and every thumbnail are lazy with async decoding.
- Each highlight (rail, swatches, sizes) and the stage run one ResizeObserver; the lens reuses the current image at zoom size.
- The viewer is portaled to the body only while open.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Use on a product detail page. Put the name, price, and reviews in header and purchase buttons in footer.
- Give each color its own photos; if a size is sold out in the new color, the selection clears through onSizeChange.
- Photos are plain img tags; pass optimized src and srcSet URLs. Set magnifier={false} for photos without fine detail.
- For a general photo grid with a lightbox use lightbox-gallery; for a simple slide rotation use carousel.
The full library index for assistants is at /llms.txt.