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):
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">import</span><span style="color:#E1E4E8"> ProductGallery </span><span style="color:#F97583">from</span><span style="color:#9ECBFF"> 'spoko-design-system/components/Product/ProductGallery.astro'</span></span></code></pre>
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
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="astro"><code><span class="line"><span style="color:#E1E4E8"><</span><span style="color:#79B8FF">ProductGallery</span></span>
<span class="line"><span style="color:#B392F0"> images</span><span style="color:#E1E4E8">={[</span></span>
<span class="line"><span style="color:#E1E4E8"> { src: </span><span style="color:#9ECBFF">"/img/product-1.avif"</span><span style="color:#E1E4E8">, thumb: </span><span style="color:#9ECBFF">"/img/product-1-thumb.avif"</span><span style="color:#E1E4E8">, full: </span><span style="color:#9ECBFF">"/img/product-1-full.jpg"</span><span style="color:#E1E4E8">, alt: </span><span style="color:#9ECBFF">"Product image 1"</span><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#E1E4E8"> { src: </span><span style="color:#9ECBFF">"/img/product-2.avif"</span><span style="color:#E1E4E8">, thumb: </span><span style="color:#9ECBFF">"/img/product-2-thumb.avif"</span><span style="color:#E1E4E8">, full: </span><span style="color:#9ECBFF">"/img/product-2-full.jpg"</span><span style="color:#E1E4E8">, alt: </span><span style="color:#9ECBFF">"Product image 2"</span><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#E1E4E8"> ]}</span></span>
<span class="line"><span style="color:#E1E4E8">/></span></span></code></pre>
Many images (10)
With badges slot
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="astro"><code><span class="line"><span style="color:#E1E4E8"><</span><span style="color:#79B8FF">ProductGallery</span><span style="color:#B392F0"> images</span><span style="color:#E1E4E8">={images}></span></span>
<span class="line"><span style="color:#E1E4E8"> <</span><span style="color:#79B8FF">Badges</span><span style="color:#B392F0"> badges</span><span style="color:#E1E4E8">={product.badges} </span><span style="color:#B392F0">class</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"top-2"</span><span style="color:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#E1E4E8"></</span><span style="color:#79B8FF">ProductGallery</span><span style="color:#E1E4E8">></span></span></code></pre>
Single image (no thumbnails)
No images (empty state)
Custom aspect ratio
Props
|
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
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#F97583">interface</span><span style="color:#B392F0"> GalleryImage</span><span style="color:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#FFAB70"> src</span><span style="color:#F97583">:</span><span style="color:#79B8FF"> string</span><span style="color:#E1E4E8">; </span><span style="color:#6A737D">// Main image URL (used in slider) β required</span></span>
<span class="line"><span style="color:#FFAB70"> thumb</span><span style="color:#F97583">?:</span><span style="color:#79B8FF"> string</span><span style="color:#E1E4E8">; </span><span style="color:#6A737D">// Thumbnail URL (falls back to `src`)</span></span>
<span class="line"><span style="color:#FFAB70"> full</span><span style="color:#F97583">?:</span><span style="color:#79B8FF"> string</span><span style="color:#E1E4E8">; </span><span style="color:#6A737D">// Full-res URL for dialog lightbox (falls back to `src`)</span></span>
<span class="line"><span style="color:#FFAB70"> alt</span><span style="color:#F97583">:</span><span style="color:#79B8FF"> string</span><span style="color:#E1E4E8">; </span><span style="color:#6A737D">// Alt text β required</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre>
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
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="astro"><code><span class="line"><span style="color:#E1E4E8"><</span><span style="color:#79B8FF">ProductGallery</span></span>
<span class="line"><span style="color:#B392F0"> images</span><span style="color:#E1E4E8">={product.images.</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">img</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">i</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> ({</span></span>
<span class="line"><span style="color:#E1E4E8"> src: img.avif_1200 </span><span style="color:#F97583">||</span><span style="color:#E1E4E8"> img.webp_1200,</span></span>
<span class="line"><span style="color:#E1E4E8"> thumb: img.avif_320 </span><span style="color:#F97583">||</span><span style="color:#E1E4E8"> img.avif_640,</span></span>
<span class="line"><span style="color:#E1E4E8"> full: img.original </span><span style="color:#F97583">||</span><span style="color:#E1E4E8"> img.url,</span></span>
<span class="line"><span style="color:#E1E4E8"> alt: </span><span style="color:#9ECBFF">`${</span><span style="color:#E1E4E8">product</span><span style="color:#9ECBFF">.</span><span style="color:#E1E4E8">number</span><span style="color:#9ECBFF">} - ${</span><span style="color:#E1E4E8">product</span><span style="color:#9ECBFF">.</span><span style="color:#E1E4E8">name</span><span style="color:#9ECBFF">} [${</span><span style="color:#E1E4E8">i</span><span style="color:#F97583"> +</span><span style="color:#79B8FF"> 1</span><span style="color:#9ECBFF">}/${</span><span style="color:#E1E4E8">product</span><span style="color:#9ECBFF">.</span><span style="color:#E1E4E8">images</span><span style="color:#9ECBFF">.</span><span style="color:#79B8FF">length</span><span style="color:#9ECBFF">}]`</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8"> }))}</span></span>
<span class="line"><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8"> <</span><span style="color:#79B8FF">Badges</span><span style="color:#B392F0"> badges</span><span style="color:#E1E4E8">={product.badges} </span><span style="color:#B392F0">class</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"top-2"</span><span style="color:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#E1E4E8"></</span><span style="color:#79B8FF">ProductGallery</span><span style="color:#E1E4E8">></span></span></code></pre>
Static asset paths
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="astro"><code><span class="line"><span style="color:#E1E4E8"><</span><span style="color:#79B8FF">ProductGallery</span></span>
<span class="line"><span style="color:#B392F0"> images</span><span style="color:#E1E4E8">={images.</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">((</span><span style="color:#FFAB70">img</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">i</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">=></span><span style="color:#E1E4E8"> ({</span></span>
<span class="line"><span style="color:#E1E4E8"> src: </span><span style="color:#9ECBFF">`/photos/${</span><span style="color:#E1E4E8">img</span><span style="color:#9ECBFF">.</span><span style="color:#E1E4E8">path</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8"> thumb: </span><span style="color:#9ECBFF">`/photos/${</span><span style="color:#E1E4E8">img</span><span style="color:#9ECBFF">.</span><span style="color:#E1E4E8">path</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8"> full: optimizedImages[i].src,</span></span>
<span class="line"><span style="color:#E1E4E8"> alt: </span><span style="color:#9ECBFF">`${</span><span style="color:#E1E4E8">productNumber</span><span style="color:#9ECBFF">} - ${</span><span style="color:#E1E4E8">productTitle</span><span style="color:#9ECBFF">} [${</span><span style="color:#E1E4E8">i</span><span style="color:#F97583"> +</span><span style="color:#79B8FF"> 1</span><span style="color:#9ECBFF">}/${</span><span style="color:#E1E4E8">images</span><span style="color:#9ECBFF">.</span><span style="color:#79B8FF">length</span><span style="color:#9ECBFF">}]`</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8"> }))}</span></span>
<span class="line"><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8"> <</span><span style="color:#79B8FF">Badges</span><span style="color:#B392F0"> badges</span><span style="color:#E1E4E8">={badges} </span><span style="color:#B392F0">class</span><span style="color:#E1E4E8">=</span><span style="color:#9ECBFF">"top-2"</span><span style="color:#E1E4E8"> /></span></span>
<span class="line"><span style="color:#E1E4E8"></</span><span style="color:#79B8FF">ProductGallery</span><span style="color:#E1E4E8">></span></span></code></pre>