Gallery✨ Premium

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.

JSNavigationKeyboard

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

Pages of an online comic, read one by one with the keyboard — Lightbox with Navigation example 1

① Pages of an online comic, read one by one with the keyboard

WhenOn the Tressaline series site, the reader opens the third page of episode 7 and turns pages with the right arrow, without leaving the viewer.
WhyA comic page is a tall format that needs to be seen large; the loop and the “3 / 4” counter give the position within the episode, and the keyboard saves aiming at the arrows.
SettingsFour-column grid in a 5:7 format, --nlb-backdrop: rgba(8, 10, 22, 0.94) for a night background. Capture: the scene opens the page then sends an ArrowRight keydown to the dialog.
Three receipts attached to an expense report line — Lightbox with Navigation example 2

② Three receipts attached to an expense report line

WhenIn Dravellon, the manager checks Camille R.'s client visit: she clicks one of the three tiny receipts, then moves to the other two with ›.
WhyThe attachments only take 36 px of width in the expense line; the viewer makes them readable and the “1 / 3” counter confirms none was missed before approving.
SettingsRoot set to display: flex inside the line, 36 px thumbnails in a 5:7 format, counter set to 3 in the HTML (the script recomputes it anyway).
Listing for a second-hand armchair: one large photo and three details — Lightbox with Navigation example 3

③ Listing for a second-hand armchair: one large photo and three details

WhenOn the Garancelle resale shop listing, the buyer opens the main photo, then swipes with a finger to see the side view, the buttoned back and the velvet.
WhyOn a listing you want to check the object from every angle; swiping is the natural phone gesture and the large photo remains the listing's showcase.
SettingsThree-column grid, first thumbnail across the full width (a main class added by the scene). Capture: a pointerdown then a pointerup near its left edge (a swipe across almost the whole image width) are sent to the image.

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's alt moves 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).

Chrome 88+✓ Full (measured)
Firefox 98+✓ Full (measured)
Safari 15.4+✓ Full (measured on WebKit)
Edge 88+✓ Full (Chromium engine)
Mobile iOS 15.4+✓ Full (not measured on a device)
Android Chrome 88+✓ Full (touch measured)

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

index.html — structure
<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 / propertyDefaultEffect
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

In show(), replace (i + thumbs.length) % thumbs.length with Math.max(0, Math.min(thumbs.length - 1, i)): the arrows then stop at both ends.
Yes: each image keeps its proportions within 80 % of the window width and 70 % of its height, and the arrows follow its edges. For a fixed frame, give .nlb-image a width and height with object-fit: contain.
No. Each .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.