Start typing to search components and docs…
↡ select ↑ ↓ navigate ESC close

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 shape

interface GalleryImage {
  src: string;    // Main image URL (used in slider) β€” required
  thumb?: string; // Thumbnail URL (falls back to `src`)
  full?: string;  // Full-res URL for dialog lightbox (falls back to `src`)
  alt: string;    // Alt text β€” required
}

Slots

Slot Description
default Overlay content (e.g. badges) positioned inside the main slider area
empty Custom content when images is empty

Integration examples

Multi-resolution CDN images

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

Static asset paths

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