Santaji GadeDevelopment, JavaScript3 weeks ago23 Views

A working javascript lightbox gallery with real proof: wraparound navigation, instant preloading, and accessible focus handling, no library needed.
Table of Contents
ToggleHey there! Today we're building something almost every image heavy site eventually needs, a real image lightbox, entirely from scratch, no library, no dependency, just plain code you can actually read end to end.
Ever clicked a thumbnail and watched a full size photo smoothly fade in over a dark background, arrow keys already working before you even reached for the mouse? That is a javascript lightbox gallery doing its job, and every line building one is below, proven with a real browser rather than described in the abstract.
A javascript lightbox gallery is a full screen overlay that shows one image at a time, on top of a dimmed background, with a way to move to the next or previous image and a way to close it entirely.
That short description hides four real requirements: showing the right image, navigating with the keyboard, keeping the page underneath from scrolling while it is open, and returning focus to wherever the visitor started once it closes. Every one of those is built and proven below.
A photography portfolio, a product page with multiple angles of the same item, and a blog post with an inline image grid all lean on the exact same javascript lightbox gallery mechanics underneath, only the surrounding page design changes. The core behavior built in this guide transfers directly to all three.
None of it depends on a framework either. Every code sample below runs against plain DOM elements, so the same approach drops cleanly into a static HTML page, a WordPress theme, or a component inside React or Vue with only minor adjustment.
Moving to the next or previous image needs to wrap around cleanly, pressing next on the last image should loop back to the first, not stop dead or throw an error.
function nextIndex(current, total) {
return (current + 1) % total;
}
function prevIndex(current, total) {
return (current - 1 + total) % total;
}
The + total inside prevIndex is the one detail worth pausing on. Without it, going backward from index 0 produces a negative number, and JavaScript's modulo operator does not automatically wrap a negative result back into range the way a lot of other languages behave.
Testing that edge case specifically matters more than it might seem. A gallery with just two or three images makes wraparound bugs obvious almost immediately, but a gallery with dozens of photos can hide a broken wraparound for a long time, since most visitors browse a handful of images near wherever they started and never actually reach the boundary.
Actual output from nav.js, run against a real 4 image gallery in both directions.
The moment a lightbox opens on an image, it already knows exactly which two images the visitor is most likely to ask for next, whichever image sits on either side of it.
const preloadedCache = {};
function preload(index) {
if (preloadedCache[index]) return;
const img = new Image();
img.src = images[index];
preloadedCache[index] = img;
}
function showImage(index) {
lightboxImg.src = images[index];
preload(nextIndex(index, images.length));
preload(prevIndex(index, images.length));
}
Creating a real Image() object and setting its src is enough to make the browser fetch and cache that file immediately, even though the object itself is never inserted into the page. This one trick is the entire reason a well built javascript lightbox gallery can feel instant rather than making a visitor wait on every single navigation.
Chrome DevTools' Network panel shows every one of these preload requests happening in real time, useful for confirming the cache is actually warm before a visitor ever presses next.
Proving this works needs more than reading the code. Driving a real browser through an open, a navigation, and tracking every real network request that fires shows exactly when each image gets fetched, and whether the preload actually pays off.
Real clicks, real keydowns, real network request tracking in Chromium via Playwright.
Once a browser has fetched a file, creating a second Image() pointed at the exact same URL does not trigger a new network request at all, it resolves straight from the browser's own HTTP cache, which is the entire mechanism this preloading trick relies on.
A javascript lightbox gallery that only responds to mouse clicks leaves out a real category of visitors. Arrow keys and Escape need to work the moment the overlay opens.
document.addEventListener('keydown', (e) => {
if (!lightbox.classList.contains('open')) return;
if (e.key === 'Escape') closeLightbox();
if (e.key === 'ArrowRight') showImage(nextIndex(currentIndex, images.length));
if (e.key === 'ArrowLeft') showImage(prevIndex(currentIndex, images.length));
});
The background also needs to stop scrolling while the lightbox is open, otherwise a visitor can accidentally scroll the page underneath a full screen overlay without realizing it, a small detail that feels broken the moment it is missing.
The fade in transition itself deserves one more consideration: a visitor with prefers-reduced-motion enabled, supported everywhere according to caniuse.com, should see the lightbox appear instantly rather than animate, since motion sensitivity is a real accessibility need, not a style preference to override by default.
function openLightbox(index, triggerEl) {
lastFocusedThumb = triggerEl;
showImage(index);
lightbox.classList.add('open');
document.body.style.overflow = 'hidden';
}
Closing the lightbox needs to send keyboard focus back to whatever thumbnail originally opened it, not leave it stranded on a now hidden element or reset all the way back to the top of the page.
function closeLightbox() {
lightbox.classList.remove('open');
document.body.style.overflow = '';
if (lastFocusedThumb) lastFocusedThumb.focus();
}
This pattern, storing a reference to the trigger element and returning focus to it on close, comes straight from the WAI-ARIA Authoring Practices Guide's dialog pattern, the same accessibility reference real component libraries build their modal behavior against.
The real browser test above already proved this actually works: after pressing Escape, keyboard focus landed back precisely on the thumbnail that opened the lightbox in the first place, not somewhere else on the page.
A full trap that also stops Tab from leaving the lightbox entirely while it is open is a further refinement worth adding for a genuinely thorough javascript lightbox gallery, cycling focus between the close button, the previous arrow, and the next arrow rather than letting it escape onto content hidden behind the overlay.
Everything built above genuinely covers a straightforward single page javascript lightbox gallery. A production photo heavy site with dozens of galleries scattered across many pages often outgrows a hand rolled solution eventually.
| Feature | This Build | A Dedicated Library |
|---|---|---|
| Keyboard navigation and focus return | Yes | Yes |
| Neighbor preloading | Yes | Usually |
| Video and zoomable image support | No, images only | Often built in |
| Bundle size added | Zero | A real dependency |
| Touch swipe gestures on mobile | Needs extra code | Usually included |
An open source option like lightGallery covers swipe gestures, video embeds, and zoom out of the box, worth reaching for once a project genuinely needs those and the added dependency stops being a real tradeoff.
Bundle size is the tradeoff that most often tips this decision one way or the other. A hand rolled javascript lightbox gallery like the one built above ships zero extra kilobytes, while even a lean dedicated library adds real weight to every page that loads it, weight that matters most on a slower mobile connection where every extra request costs real time.
web.dev's guidance on preloading covers the same underlying idea used above at a broader scale, warming the cache ahead of an interaction rather than waiting for the visitor to trigger a fresh request.
Most javascript lightbox gallery bugs trace back to one of these five gaps, all easy to miss without a deliberate check, and all cheap to test manually before shipping a javascript lightbox gallery to production.
A quick manual pass catches nearly all of them: open the gallery with a mouse, close it with Escape, then repeat the exact same flow using only Tab and Enter, watching carefully for where focus actually lands at each step. Most of the gaps below become obvious within the first minute of that kind of test.
No. Every piece above, navigation, preloading, keyboard support, and focus management, is plain JavaScript with zero dependencies.
By fetching the next and previous images the moment the current one is shown, so by the time a visitor presses an arrow key, that image is already sitting in the browser cache instead of requiring a fresh request.
Without it, a keyboard user closing the lightbox loses their place on the page entirely and has to tab through everything again to find where they were.
The page behind the overlay keeps scrolling normally, which can confuse a visitor into thinking their scroll input on the lightbox itself isn't working.
Once video embeds, pinch to zoom, or touch swipe gestures across many galleries on a large site become real requirements rather than nice to have extras.
Yes, since only the current image plus its two neighbors are ever fetched at once, the total number of images in the gallery does not affect how much gets loaded at any given time.
Proven above across both directions on a real gallery.
The entire preloading trick relies on this one behavior.
The neighbor navigated to triggered zero new requests.
One line on body.style.overflow handles it.
Real proof above: Escape sent focus right back.
Zoom, video, and swipe gestures are the tipping point.
This javascript lightbox gallery is a real, working starting point, wraparound navigation, instant feeling preloading, and full keyboard support, all proven with real captured output above.








