Swipe Gallery
Viewer built as a track of slides you pull with a finger or the mouse: the photo follows the gesture, then snaps to the next one beyond 50 px.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Gallery has 44 effects, including 6 free. Explore the category →
3 usage examples



How it works
On the first opening, the script builds one .slb-slide per thumbnail (with the data-full image, otherwise the thumbnail's, and the same alt) and lines them up in .slb-track. Large images are therefore only loaded if someone opens the gallery. The “2 / 4” counter is computed from the number of thumbnails.
Dragging uses Pointer Events with pointer capture: finger, pen and mouse behave alike, and the mouse can leave the area without cancelling the gesture. During the gesture the track follows the pointer with no delay; on release, more than 50 px to the left moves to the next slide, more than 50 px to the right goes back, otherwise the track returns into place with a small bounce (0.4 s). No loop: it stops at the first and the last slide.
A slide is at most 600 px wide, 80 % of the window, or whatever 70 % of its height allows at 3:2; the photo is shown whole (object-fit: contain). Under the track, the “Swipe to navigate” hint swings its arrow three times, then stops.
Accessibility
- Keyboard: thumbnails are
<button>elements; inside the viewer, ← and → change slide, Escape closes. There are no on-screen arrow buttons: with a mouse, you pull the track. - Screen readers: only the slide on display is readable (the others carry
aria-hidden="true"); the “2 / 4” counter is anaria-live="polite"region. - Modal dialog: named “Image viewer”, focus on ✕ on opening, page behind inert, focus returned to the thumbnail on closing.
- Reduced motion: no zoom on opening, no animated slide between slides, the hint arrow stays still. The gesture stays active: the track still follows the finger.
- Vertical gesture:
touch-action: pan-ylets the page scroll vertically; a vertical move cancels the drag and the track returns into place.
Browser compatibility
Pointer Events and setPointerCapture: Chrome 55, Firefox 59, Safari 13; ResizeObserver realigns the track when the window changes (Chrome 64, Firefox 69, Safari 13.1). The rest follows the viewers' base (modal <dialog>: Firefox 98, Safari 15.4). Measured in Chromium 151, Firefox 153 and WebKit 26.5: mouse drag leaving the area, arrows without loop, touch swipe at 390 px in Chromium.
Without ResizeObserver, the position is recomputed at each opening but not during a resize. Without showModal(), the gallery stays a row of photos.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0178-root">
<div class="slb-grid">
<button type="button" class="slb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Pink to violet gradient" width="600" height="400"></button>
<button type="button" class="slb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Amber to red gradient" width="600" height="400"></button>
<button type="button" class="slb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Fuchsia to rose gradient" width="600" height="400"></button>
<button type="button" class="slb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Violet to cyan gradient" width="600" height="400"></button>
</div>
<dialog class="slb-viewer" aria-label="Image viewer">
<div class="slb-content">
<p class="slb-counter" aria-live="polite"><span class="slb-current">1</span> / <span class="slb-total">4</span></p>
<button type="button" class="slb-close" aria-label="Close"><svg>…</svg></button>
<div class="slb-viewport">
<div class="slb-track"></div>
</div>
<p class="slb-hint"><span>Swipe to navigate</span><svg>…</svg></p>
</div>
</dialog>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Change threshold (JS — dx < -50 / dx > 50) |
50 px | Distance to pull to reach the neighbouring slide. |
Slide width (CSS — .slb-viewport) |
min(600px, 80vw, calc((100vh - 140px) * 1.5)) |
The vh calculation keeps room, at 3:2, for the counter and ✕ above the image and the hint below it, even in a short window; slides always take 100 % of this width. |
Slide format (CSS — aspect-ratio of .slb-slide) |
3 / 2 | Match it to your photos; change the 1.5 in the width calculation accordingly. |
Gap and bounce (CSS — .slb-track) |
20 px, 0.4 s cubic-bezier(0.175, 0.885, 0.32, 1.275) |
The gap is read back by the script, nothing else to change. |
Help text (HTML — .slb-hint) |
“Swipe to navigate” | Edit or remove it; the arrow swings three times (animation: slb-hint 1.5s … 3). |
Thumbnails and overlay (CSS — --slb-thumb, --slb-backdrop) |
120px, rgba(0, 0, 0, 0.9) |
Width of the page buttons and viewer background. |
Large photo (HTML — data-full) |
absent | Image placed in the slide; loaded on the first opening only. |
FAQ
Math.max(0, Math.min(slides.length - 1, i)) in show() with (i + slides.length) % slides.length; dragging will then loop too.touch-action: pan-y). A vertical gesture lets the page scroll and the track goes back into place.