Day 20: Image Lightbox / Gallery Viewer
Build an image gallery where clicking a thumbnail opens a lightbox, displays the selected image in a larger view, and allows the user to move between images with next and previous controls.
JavaScript focus
- selecting multiple gallery items
- tracking active index
- updating image src, alt, and optional caption
- opening and closing viewer state
- handling next/previous controls
- handling keyboard events
- keeping DOM updates in sync with state
Nice extras
- show a caption for the active image
- show current position like “2 of 6”
- support keyboard navigation with left/right arrows
- hide or disable prev/next buttons at the ends
- preload the next image
- use a consistent body class while the lightbox is open
- animate image transitions lightly
- support multiple galleries later if you want to expand it
MDN prep
Image Lightbox / Gallery Viewer
The HTML
<section class="section lightbox_gallery-viewer">
<h2 class="secondary_heading">Image Lightbox / Gallery Viewer</h2>
<div class="gallery_wrap">
<button aria-label="See Image" type="button" aria-selected="false" class="lightbox_trigger">
<figure class="figure">
<img class="gallery_img" src="../images/gallery/brooke-lark-qdyBKWSzpSI-unsplash.jpg" alt="Smoothies on a blue tile surface.">
</figure>
</button>
<button aria-label="See Image" type="button" aria-selected="false" class="lightbox_trigger">
<figure class="figure">
<img class="gallery_img" src="../images/gallery/naveed-pervaiz-IlnF2g_3tpY-unsplash.jpg" alt="A fruit charcuterie board.">
</figure>
</button>
<button aria-label="See Image" type="button" aria-selected="false" class="lightbox_trigger">
<figure class="figure">
<img class="gallery_img" src="../images/gallery/corina-rainer-sScNrKruEPs-unsplash.jpg" alt="Glass of white wine.">
</figure>
</button>
<button aria-label="See Image" type="button" aria-selected="false" class="lightbox_trigger">
<figure class="figure">
<img class="gallery_img" src="../images/gallery/emre-NZMeJsrMC8U-unsplash.jpg" alt="A cup of espresso.">
</figure>
</button>
<button aria-label="See Image" type="button" aria-selected="false" class="lightbox_trigger">
<figure class="figure">
<img class="gallery_img" src="../images/gallery/diliara-garifullina-I48gnI1Qs5o-unsplash.jpg" alt="A slice of raspberry cheesecake.">
</figure>
</button>
<button aria-label="See Image" type="button" aria-selected="false" class="lightbox_trigger">
<figure class="figure">
<img class="gallery_img" src="../images/gallery/t-j-breshears-Hi86bgXS4iE-unsplash.jpg" alt="Watermelon slices.">
</figure>
</button>
<button aria-label="See Image" type="button" aria-selected="false" class="lightbox_trigger">
<figure class="figure">
<img class="gallery_img" src="../images/gallery/heather-barnes-CNDiESvWfrk-unsplash.jpg" alt="A honey dipper in honey with a slice of lemon.">
</figure>
</button>
<button aria-label="See Image" type="button" aria-selected="false" class="lightbox_trigger">
<figure class="figure">
<img class="gallery_img" src="../images/gallery/aliona-gumeniuk-jeAjT87nbjM-unsplash copy.jpg" alt="Parmesan and pomegranate.">
</figure>
</button>
<button aria-label="See Image" type="button" aria-selected="false" class="lightbox_trigger">
<figure class="figure">
<img class="gallery_img" src="../images/gallery/shelley-pauls-I58f47LRQYM-unsplash.jpg" alt="A bundle of honeycrisp apples.">
</figure>
</button>
<button aria-label="See Image" type="button" aria-selected="false" class="lightbox_trigger">
<figure class="figure">
<img class="gallery_img" src="../images/gallery/monika-grabkowska-EbRBhZ-I_p8-unsplash.jpg" alt="A platter of sliced blueberry cheesecake. ">
</figure>
</button>
<button aria-label="See Image" type="button" aria-selected="false" class="lightbox_trigger">
<figure class="figure">
<img class="gallery_img" src="../images/gallery/steve-doig-FaMBWkmvPyY-unsplash.jpg" alt="Lemons on a lemon tree.">
</figure>
</button>
</div>
<dialog class="lightbox_dialog">
<button type="button" class="close_lightbox lightbox_button" aria-label="Close Lightbox">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path fill="none" stroke="" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M368 368L144 144M368 144L144 368"/></svg>
</button>
<div class="buttons_container">
<button class="lightbox_button prev_button" aria-label="Previous Image">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path fill="none" stroke="" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M328 112L184 256l144 144"/></svg>
</button>
<button class="lightbox_button next_button" aria-label="Next Image">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path fill="none" stroke="" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M184 112l144 144-144 144"/></svg>
</button>
</div>
</dialog>
</section>
The JavaScript
function initLightBox() {
const body = document.body;
const root = document.querySelector(".lightbox_gallery-viewer");
if (!root) return;
const buttons = root.querySelectorAll(".lightbox_trigger");
const dialog = root.querySelector(".lightbox_dialog");
const closeLightbox = root.querySelector(".close_lightbox");
const next = root.querySelector(".next_button");
const prev = root.querySelector(".prev_button");
const totalImages = buttons.length;
let currentIndex = 0;
const lightboxFigure = document.createElement("figure");
lightboxFigure.classList.add("figure");
const lightboxImage = document.createElement("img");
lightboxFigure.append(lightboxImage);
const updateImages = () => {
const currentButton = buttons[currentIndex];
const currentImg = currentButton.querySelector(".gallery_img");
lightboxImage.src = currentImg.src;
lightboxImage.alt = currentImg.alt;
buttons.forEach((button, index) => {
button.setAttribute("aria-selected", index === currentIndex ? "true" : "false");
});
};
const openLightbox = () => {
body.classList.add("lightbox_open");
if (!dialog.contains(lightboxFigure)) {
dialog.append(lightboxFigure);
}
updateImages();
dialog.showModal();
};
const closeDialog = () => {
body.classList.remove("lightbox_open");
dialog.close();
buttons.forEach((button) => {
button.setAttribute("aria-selected", "false");
});
};
const showNext = () => {
currentIndex = (currentIndex + 1) % totalImages;
updateImages();
};
const showPrev = () => {
currentIndex = (currentIndex - 1 + totalImages) % totalImages;
updateImages();
};
buttons.forEach((button, index) => {
button.setAttribute("aria-label", `Image ${index + 1} of ${totalImages}`);
button.setAttribute("aria-selected", "false");
button.addEventListener("click", () => {
currentIndex = index;
openLightbox();
});
});
next?.addEventListener("click", showNext);
prev?.addEventListener("click", showPrev);
closeLightbox?.addEventListener("click", closeDialog);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && dialog.open) {
closeDialog();
}
});
}
initLightBox();