Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Commerce

Product gallery

Commerce

Product gallery React component for Tailwind CSS v4

Pricing tableQuantity stepper
Live props
State
defaultIndex0
defaultOpen
Layout
thumbnails
aspect
maxThumbnails6
bleedOnMobile
Appearance
fit
indicator
showArrows
radius
Behavior
zoom
zoomScale2x
lightbox
loop
Code
import { ProductGallery } from "@/components/ui/product-gallery"

<ProductGallery />
  1. Home
  2. Components
  3. Commerce
  4. Product gallery
Pro

Product gallery React component for Tailwind CSS v4

v1.0.0·Added 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-gallery

Product 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.

  • defaultIndexdefault 0
    number

    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.

  • defaultOpendefault false
    boolean

    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.

  • 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.

  • zoomScaledefault 2
    number

    How far the hover zoom or the lens magnifies.

  • lightboxdefault true
    boolean

    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.

  • 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.

  • showArrowsdefault true
    boolean

    Previous / next buttons over the stage for mouse and trackpad users, shown on hover or keyboard focus; touch screens swipe instead.

  • loopdefault false
    boolean

    Wraps from the last photo to the first, on the stage and in the viewer.

  • maxThumbnailsdefault 6
    number

    How 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.

  • bleedOnMobiledefault false
    boolean

    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.

Dependencies

lucide-reactmotionbuttoncarouselimage-lightboxuse-controllable-stateuse-media-querymotionui-config

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

Image LightboxCarouselListing cardSeller cardFavorite button

Used in

Listing Detail PageStorefront Page

More React commerce components

Order trackerCheckout stepsCart drawerListing gridPricing tableReviews
All 16 React commerce components

Articles using this component

  • Shadcn Ecommerce Template: Five Screens That Work on Phones

    Build it: screens and apps · Nov 6, 2026 · 11 min read

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

    Component guides · Oct 19, 2026 · 9 min read

PreviousPricing tableNext Quantity stepper
Bright living room with a gray sofa, a bookcase and plants
Green velvet sofa by a white wall
Boulevard with tall buildings seen from above
Wooden desk with a laptop and a plant
Snowy peaks and fir forests
White bike leaning on a dark wall
Floor plan: living room, bedroom, kitchen, hall, bath and balcony
Map with the building on the Upper West Side, near Riverside Park
Bright living room with a gray sofa, a bookcase and plants
Promoted
Zoom with the mouse, click for full screen
1 / 8

New York, Upper West Side · Yesterday at 9:10 AM

1-bedroom apartment, renovated

$489,000, $843/sq ft

photo 1 / 8 · zoom="hover" · zoomScale=2 · lightbox=true · loop=false