Lightbox with Navigation
Viewer that pages through the whole gallery: a “2 / 4” counter, ‹ › arrows that loop, ← → keys and a finger swipe on the image.
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
The gallery is a series of .nlb-thumb buttons, each holding its photo. The script counts them when the page loads and writes that number into .nlb-total: add a sixth photo and the counter reads “/ 6” with no other change. Clicking a thumbnail opens the modal <dialog> on that very image.
Inside the viewer, ‹ and › (or ← and → on the keyboard) move to the neighbouring image; after the last one it goes back to the first, and the other way round. On the image, a horizontal drag of more than 50 px with a finger, pen or mouse does the same (Pointer Events; vertical moves are ignored and pinch-zoom stays allowed). The large picture comes from data-full when the button has one, otherwise from the thumbnail.
Above 768 px wide the arrows float 70 px on either side of the image; below that they move inside it, on a dark disc, so they never leave a phone screen. Opening: the background fades in and the image grows from 80 to 100 % in 0.4 s.
Accessibility
- Real buttons throughout: thumbnails, ‹, › and ✕ are
<button>elements with a name (“Previous image”, “Next image”, “Close”). - Modal window:
showModal()gives the dialog role, makes the page behind inert and puts focus on ✕; the dialog is announced as “Image viewer”. - Spoken counter: the “2 / 4” paragraph is an
aria-live="polite"region read after each change; the thumbnail'saltmoves onto the large image. - Closing: Escape, ✕ or a click on the background; focus returns to the original thumbnail.
- Reduced motion: no fade or zoom on opening. Changing image is never animated, and swiping stays active.
Browser compatibility
Same foundations as the basic viewer: modal <dialog> (Firefox 98, Safari 15.4) and aspect-ratio (Chrome and Edge 88). Pointer Events and setPointerCapture exist since Chrome 55, Firefox 59 and Safari 13. Checked in Chromium 151, Firefox 153 and WebKit 26.5, including a real touch swipe at 390 px in Chromium and the script pasted twice (→ still moves by exactly one image).
Without showModal() nothing opens and 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-0172-root">
<div class="nlb-grid">
<button type="button" class="nlb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Fuchsia to rose gradient" width="600" height="400"></button>
<button type="button" class="nlb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Violet to cyan gradient" width="600" height="400"></button>
<button type="button" class="nlb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Pink to violet gradient" width="600" height="400"></button>
<button type="button" class="nlb-thumb" aria-haspopup="dialog"><img src="photo.jpg" alt="Indigo to violet gradient" width="600" height="400"></button>
</div>
<dialog class="nlb-viewer" aria-label="Image viewer">
<div class="nlb-content">
<p class="nlb-counter" aria-live="polite"><span class="nlb-current">1</span> / <span class="nlb-total">4</span></p>
<button type="button" class="nlb-close" aria-label="Close"><svg>…</svg></button>
<button type="button" class="nlb-nav nlb-prev" aria-label="Previous image"><svg>…</svg></button>
<img class="nlb-image" alt="">
<button type="button" class="nlb-nav nlb-next" aria-label="Next image"><svg>…</svg></button>
</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 |
|---|---|---|
Thumbnail width (CSS — --nlb-thumb) |
120px | Thumbnails keep a 3:2 format; the row wraps when space runs out. |
Grid gap (CSS — --nlb-gap) |
10px | Space between two thumbnails. |
Overlay (CSS — --nlb-backdrop) |
rgba(0, 0, 0, 0.9) |
Viewer background; arrows, counter and ✕ are white, so the overlay must stay dark. |
Corners (CSS — --nlb-radius, --nlb-radius-lg) |
0.5rem, 1rem | Rounding of the thumbnails and of the large image. |
Hover colour (CSS — --nlb-accent) |
rgba(99, 102, 241, 0.3) |
Drop shadow of the thumbnail under the pointer or reached by keyboard. |
High-resolution image (HTML — data-full) |
absent | Put it on a .nlb-thumb button: the viewer loads that URL instead of the thumbnail. |
Swipe threshold (JS — Math.abs(dx) > 50) |
50 px | Minimum horizontal distance to change image with a finger or the mouse. |
Closing time (JS — CLOSE_MS) |
400 ms | Time given to the fade before the dialog closes; change it together with the CSS transition. |
FAQ
show(), replace (i + thumbs.length) % thumbs.length with Math.max(0, Math.min(thumbs.length - 1, i)): the arrows then stop at both ends..nlb-image a width and height with object-fit: contain..fx-0172-root has its own viewer and its own listeners, attached to the dialog rather than the document: the right arrow only moves the viewer that is open.