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