ProductGallery

Scroll-snap based product image gallery with fullscreen dialog lightbox. Zero external dependencies — uses CSS scroll-snap, IntersectionObserver, and native <dialog>.

Import (subpath — has <script>, must NOT go in barrel):

import ProductGallery from 'spoko-design-system/components/Product/ProductGallery.astro'

Features

  • Slider — CSS scroll-snap, swipe on mobile, mouse drag on desktop
  • Thumbnails — synced via IntersectionObserver, desktop only
  • Counter — fraction (1/5), always visible
  • Arrows — hover reveal animation, desktop only
  • Dialog — fullscreen lightbox with zoom (click/wheel/double-tap) + pan
  • Keyboard — ArrowLeft/Right in dialog, Escape to close

Basic usage

Product image 1
Product image 2
Product image 3
Product image 4
Product image 5
1/5
1/5
Product image 1
Product image 2
Product image 3
Product image 4
Product image 5
<ProductGallery
  images={[
    { src: "/img/product-1.avif", thumb: "/img/product-1-thumb.avif", full: "/img/product-1-full.jpg", alt: "Product image 1" },
    { src: "/img/product-2.avif", thumb: "/img/product-2-thumb.avif", full: "/img/product-2-full.jpg", alt: "Product image 2" },
  ]}
/>

Many images (10)

Image 1
Image 2
Image 3
Image 4
Image 5
Image 6
Image 7
Image 8
Image 9
Image 10
1/10
1/10
Image 1
Image 2
Image 3
Image 4
Image 5
Image 6
Image 7
Image 8
Image 9
Image 10

With badges slot

Product with badge
Product image 2
1/2
NEW
1/2
Product with badge
Product image 2
<ProductGallery images={images}>
  <Badges badges={product.badges} class="top-2" />
</ProductGallery>

Single image (no thumbnails)

Single product image
1/1
1/1
Single product image

No images (empty state)

No images available

Custom aspect ratio

Wide image 1
Wide image 2
1/2
1/2
Wide image 1
Wide image 2

Props

PropTypeDefaultDescription
imagesGalleryImage[][]Array of image objects
aspectRatiostring"4/3"CSS aspect-ratio for the slider
classstring—Additional CSS class on root element
ariaLabelstring"Product image gallery"Accessible label for the carousel region

GalleryImage

PropertyTypeRequiredDescription
srcstringyesMain image URL (used in slider)
thumbstringnoThumbnail URL (falls back to src)
fullstringnoFull resolution URL for dialog (falls back to src)
altstringyesAlt text for the image

Slots

SlotDescription
defaultOverlay content (e.g. badges), positioned inside the main slider area
emptyCustom content when images is empty

Integration examples

catalog.polo.blue

<ProductGallery
  images={product.images.map((img, i) => ({
    src: img.avif_1200 || img.webp_1200,
    thumb: img.avif_320 || img.avif_640,
    full: img.original || img.url,
    alt: `${product.number} - ${product.name} [${i + 1}/${product.images.length}]`,
  }))}
>
  <Badges badges={product.badges} class="top-2" />
</ProductGallery>

sale.polo.blue

<ProductGallery
  images={images.map((img, i) => ({
    src: `/photos/${img.path}`,
    thumb: `/photos/${img.path}`,
    full: optimizedImages[i].src,
    alt: `${productNumber} - ${productTitle} [${i + 1}/${images.length}]`,
  }))}
>
  <Badges badges={badges} class="top-2" />
</ProductGallery>