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
1/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)
1/10
With badges slot
1/2
NEW <ProductGallery images={images}>
<Badges badges={product.badges} class="top-2" />
</ProductGallery>
Single image (no thumbnails)
1/1
No images (empty state)
No images available
Custom aspect ratio
1/2
Props
| Prop | Type | Default | Description |
|---|---|---|---|
images | GalleryImage[] | [] | Array of image objects |
aspectRatio | string | "4/3" | CSS aspect-ratio for the slider |
class | string | — | Additional CSS class on root element |
ariaLabel | string | "Product image gallery" | Accessible label for the carousel region |
GalleryImage
| Property | Type | Required | Description |
|---|---|---|---|
src | string | yes | Main image URL (used in slider) |
thumb | string | no | Thumbnail URL (falls back to src) |
full | string | no | Full resolution URL for dialog (falls back to src) |
alt | string | yes | Alt text for the image |
Slots
| Slot | Description |
|---|---|
| default | Overlay content (e.g. badges), positioned inside the main slider area |
empty | Custom 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>