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'
Initialize β import the auto-init once, e.g. in the layout of pages that render galleries:
<script>
import 'spoko-design-system/scripts/product-gallery-auto';
</script>
Each gallery registers itself on window.__sdsGalleryQueue with a tiny inline script, and the
auto-init wires up every registered gallery β including ones rendered after it loads and pages
swapped in by Astro view transitions. Initialization is idempotent, so the import can sit in a
shared layout. To initialize by hand instead (e.g. HTML injected via innerHTML, where inline
scripts donβt run), call initProductGallery(root):
import { initProductGallery } from 'spoko-design-system/scripts/product-gallery';
document.querySelectorAll('[data-product-gallery]').forEach(initProductGallery);
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
<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)
With badges slot
<ProductGallery images={images}>
<Badges badges={product.badges} class="top-2" />
</ProductGallery>
Single image (no thumbnails)
No images (empty state)
No images available
Custom aspect ratio
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>