Image Gallery with Lightbox

Build a responsive image gallery with CSS Grid, smooth lightbox preview, transitions, and keyboard navigation using vanilla JavaScript

Time to implement the project: ~ 18-26 hours

  • HTML Structure
  • CSS Grid
  • Responsive Layout
  • Vanilla JavaScript
  • Lightbox Modal
  • CSS Transitions
  • Keyboard Navigation
  • Accessibility Basics

In this intermediate-level project, you will build a responsive image gallery with a lightbox modal using HTML, CSS Grid, and vanilla JavaScript. The gallery should display a collection of images in a flexible grid, adapt smoothly to different screen sizes, and allow users to open any image in a larger preview mode without leaving the page. The result should feel like a real portfolio, photography showcase, product gallery, or editorial media section.

This project is more advanced than a basic static gallery because it requires interaction design, state handling, modal behavior, transitions, and keyboard support. Users should be able to click an image, view it in a lightbox, move to the next or previous image, close the modal, and use keyboard controls such as Escape, ArrowLeft, and ArrowRight. You will practice building a polished UI component without relying on external libraries, which is excellent training for understanding how interactive frontend behavior works at a lower level.

Project Purpose and Practical Value

The purpose of this project is to help you move beyond simple layouts and learn how to build a complete interactive component from scratch. A gallery with lightbox may seem visually simple, but it includes many real frontend responsibilities: responsive layout decisions, click event handling, modal state, image indexing, navigation logic, animation timing, and accessibility considerations.

You will learn how to use CSS Grid to create a flexible gallery that works across multiple screen sizes without manually controlling every item. Instead of forcing fixed rows and columns, you will build a layout that naturally adapts to the available space. This is a practical skill for portfolios, landing pages, image-heavy blogs, product pages, real estate websites, creative agencies, and any interface that needs to display visual content professionally.

The JavaScript part will teach you how to control UI state manually. You will track which image is currently active, update the modal content when users navigate, add and remove classes for open and closed states, and listen for keyboard events only when the lightbox is active. This gives you stronger control over the browser, the DOM, and user interaction patterns before moving to framework-based implementations.

Knowledge Needed Before Starting

This is an intermediate project, so you should already be comfortable writing semantic HTML, styling layouts with CSS, and using basic JavaScript events. You do not need a framework such as React, Vue, or Angular. In fact, the project is intentionally designed for vanilla JavaScript so you can understand the core behavior behind many gallery libraries and UI plugins.

You should also be ready to think about user experience details. A good lightbox is not just an enlarged image. It needs clear controls, smooth opening and closing behavior, predictable navigation, readable focus states, and a reliable way to exit the modal. These small details separate a beginner visual exercise from a more professional interactive component.

  • Solid understanding of HTML elements, image markup, buttons, and basic accessibility attributes
  • Ability to build responsive layouts with CSS Grid, Flexbox, spacing, and media queries
  • Experience using JavaScript query selectors, event listeners, arrays, indexes, and functions
  • Understanding of CSS transitions, opacity, transform, hover states, and active classes
  • Basic knowledge of modal behavior, overlay layers, close buttons, and scroll locking
  • Awareness of keyboard interactions, focus management, and user-friendly navigation patterns

Core Features of the Image Gallery

The gallery should work as a complete interactive media component. Users should be able to browse images in the grid, open a selected image, move between images inside the lightbox, and close the preview easily. The implementation should remain clean, responsive, and maintainable instead of relying on overly complex scripts or duplicated markup.

Feature Implementation Focus
Responsive grid layout Use CSS Grid to arrange images in a flexible layout that adapts to desktop, tablet, and mobile screens. The gallery should avoid awkward gaps and preserve visual balance.
Clickable image items Each gallery item should be interactive and open the corresponding image in the lightbox. Store or detect the selected image index so navigation remains accurate.
Lightbox modal overlay Create a modal layer that appears above the page content, darkens the background, and displays the selected image clearly with close and navigation controls.
Next and previous controls Add buttons that let users move through the gallery without closing the modal. The logic should handle the first and last image predictably, either by looping or stopping.
Keyboard navigation Support Escape to close the lightbox and arrow keys to move between images. Keyboard behavior should only run when the modal is open to avoid unexpected page interactions.
Smooth transitions Use transitions for opening, closing, hover effects, overlay opacity, and image scaling. The motion should improve the experience without making the interface feel slow.
Caption or image metadata Optionally show image titles, captions, or counters such as “3 of 12” inside the lightbox. This helps users understand where they are in the gallery.
Accessible modal behavior Use buttons for controls, provide meaningful alt text, make close actions clear, and avoid trapping users in a modal they cannot exit with keyboard or mouse.

Implementation Guidance for Intermediate Developers

Start with a clean HTML structure and a small image data pattern. Even if you write the gallery manually in HTML, think about each image as an item with a source, alt text, title, and optional caption. This mindset makes the JavaScript easier to organize because every lightbox update should come from the currently selected gallery item rather than from hardcoded modal content.

Build the CSS Grid layout before adding JavaScript. The gallery should already look strong as a static section: consistent spacing, responsive columns, image cropping, hover states, and readable focus styles. Once the static gallery is solid, add interaction step by step: open modal, close modal, update active image, add next and previous controls, then connect keyboard events.

Be careful with modal state and event listeners. A common mistake is attaching keyboard events repeatedly every time the modal opens, which can create duplicated behavior. A cleaner approach is to register one keyboard listener and check whether the lightbox is currently active before responding. Also consider locking page scroll while the modal is open so users do not accidentally move the background content.

  • Use CSS Grid for the main gallery instead of manually sizing every row and column
  • Keep the active image index in JavaScript so next and previous controls stay reliable
  • Use real button elements for close, next, and previous controls instead of plain divs
  • Add alt text and optional captions so the gallery is more meaningful and accessible
  • Make the modal close with the Escape key, close button, and optionally overlay click
  • Prevent keyboard navigation from firing when the lightbox is not currently open
  • Test the gallery with different image sizes to avoid broken cropping or layout shifts
  • Keep transitions subtle and fast so animation supports usability instead of distracting users

Common Mistakes When Building an Image Gallery with Lightbox

1. Hardcoding every image instead of using a clear gallery data model

A gallery may start with six images, but it should not be built as six unrelated blocks of HTML. A common mistake is copying and pasting the same image markup again and again. This makes the gallery hard to update, because every image has its own manually written thumbnail, full-size image, alt text, caption, and lightbox content. If you later add filtering, categories, captions, or next/previous navigation, duplicated markup becomes difficult to maintain.

A better approach is to define image data first. Each image should have a stable ID, thumbnail source, full-size source, alt text, caption, and optional category or external link. Then the gallery can render items from that data. This keeps the HTML consistent and makes the lightbox easier to control because the selected image always comes from one predictable source.

Problematic approach:


          <div class="gallery">
            <img src="/img/photo-1.jpg" onclick="openLightbox('img/photo-1.jpg')" />
            <img src="/img/photo-2.jpg" onclick="openLightbox('img/photo-2.jpg')" />
            <img src="/img/photo-3.jpg" onclick="openLightbox('img/photo-3.jpg')" />
          </div>

This works only for a very small demo. The images have no useful alt text, the click logic is mixed into HTML, and there is no structured way to store captions or full-size image paths.

Better data model:


          const galleryImages = [
            {
              id: "mountain-lake",
              thumbnailSrc: "images/thumbs/mountain-lake.jpg",
              fullSrc: "images/full/mountain-lake.jpg",
              alt: "Mountain lake surrounded by trees",
              caption: "Morning view near the mountain lake",
              category: "Nature"
            },
            {
              id: "city-street",
              thumbnailSrc: "images/thumbs/city-street.jpg",
              fullSrc: "images/full/city-street.jpg",
              alt: "Evening city street with lights",
              caption: "City lights after sunset",
              category: "Urban"
            }
          ];

Rendering from data:


          function renderGallery(images) {
            const gallery = document.querySelector("[data-gallery]");

            gallery.innerHTML = images
              .map((image) => {
                return `
                  <button
                    class="gallery__item"
                    type="button"
                    data-image-id="${image.id}"
                    aria-label="Open image: ${image.alt}"
                  >
                    <img
                      src="/${image.thumbnailSrc}"
                      alt="${image.alt}"
                      loading="lazy"
                    />
                  </button>
                `;
              })
              .join("");
          }

Pay attention to: A gallery needs structured image data. Store thumbnails, full-size images, captions, alt text, and IDs in one place instead of scattering them through duplicated markup.

2. Opening a visual lightbox without accessible modal behavior

A lightbox is not just a large image on top of the page. It behaves like a modal dialog, which means users need a clear way to close it, keyboard focus should move into the lightbox, and screen readers should understand that a new dialog is open. A common beginner mistake is showing an overlay visually but leaving the rest of the page active behind it.

This creates several usability problems. Keyboard users may tab into links behind the overlay. Screen-reader users may not know that the lightbox opened. Mobile users may accidentally scroll the page behind the modal. A polished Image Gallery with Lightbox should handle open, close, Escape key, focus, and overlay behavior intentionally.

Problematic approach:


          <div class="lightbox">
            <img src="/images/full/mountain-lake.jpg" />
          </div>

This shows a large image, but it does not identify the overlay as a dialog, does not include a close button, and does not provide image alt text.

Better lightbox structure:


          <div
            class="lightbox"
            data-lightbox
            role="dialog"
            aria-modal="true"
            aria-labelledby="lightbox-title"
            hidden
          >
            <div class="lightbox__backdrop" data-lightbox-close></div>

            <div class="lightbox__panel">
              <button
                class="lightbox__close"
                type="button"
                data-lightbox-close
                aria-label="Close image preview"
              >
                ×
              </button>

              <h2 id="lightbox-title" class="sr-only">Image preview</h2>

              <img data-lightbox-image src="/" alt="" />

              <p class="lightbox__caption" data-lightbox-caption></p>
            </div>
          </div>

Open and close logic:


          const lightbox = document.querySelector("[data-lightbox]");
          const lightboxImage = document.querySelector("[data-lightbox-image]");
          const lightboxCaption = document.querySelector("[data-lightbox-caption]");
          const closeButtons = document.querySelectorAll("[data-lightbox-close]");

          function openLightbox(image) {
            lightboxImage.src = image.fullSrc;
            lightboxImage.alt = image.alt;
            lightboxCaption.textContent = image.caption;

            lightbox.hidden = false;
            document.body.classList.add("is-lightbox-open");

            const closeButton = lightbox.querySelector(".lightbox__close");
            closeButton.focus();
          }

          function closeLightbox() {
            lightbox.hidden = true;
            lightboxImage.src = "";
            lightboxImage.alt = "";
            document.body.classList.remove("is-lightbox-open");
          }

          closeButtons.forEach((button) => {
            button.addEventListener("click", closeLightbox);
          });

          document.addEventListener("keydown", (event) => {
            if (event.key === "Escape" && !lightbox.hidden) {
              closeLightbox();
            }
          });

Pay attention to: A lightbox should behave like a modal, not only look like one. Add dialog semantics, close controls, Escape key support, focus handling, and body-scroll control.

3. Building next and previous navigation with unsafe index logic

Lightbox navigation usually needs previous and next buttons. The common mistake is incrementing or decrementing an image index without checking boundaries. This can produce undefined images, broken sources, or buttons that continue working even when there is no next image. The problem becomes worse when galleries are filtered, reordered, or generated dynamically.

A stronger approach is to keep a selected image ID or selected index that is always derived from the current gallery array. Navigation should wrap safely or disable buttons at the beginning and end. The important part is consistency: users should never click “Next” and see a broken image.

Problematic approach:


          let selectedIndex = 0;

          function showNextImage() {
            selectedIndex += 1;

            lightboxImage.src = galleryImages[selectedIndex].fullSrc;
          }

          function showPreviousImage() {
            selectedIndex -= 1;

            lightboxImage.src = galleryImages[selectedIndex].fullSrc;
          }

This code can easily move outside the image array. It also does not update the caption or alt text together with the image.

Better selected image state:


          let selectedImageId = null;

          function getSelectedImageIndex() {
            return galleryImages.findIndex((image) => {
              return image.id === selectedImageId;
            });
          }

          function setSelectedImage(image) {
            selectedImageId = image.id;

            lightboxImage.src = image.fullSrc;
            lightboxImage.alt = image.alt;
            lightboxCaption.textContent = image.caption;
          }

Safe wrap-around navigation:


          function showNextImage() {
            const currentIndex = getSelectedImageIndex();

            if (currentIndex === -1) {
              setSelectedImage(galleryImages[0]);
              return;
            }

            const nextIndex = (currentIndex + 1) % galleryImages.length;

            setSelectedImage(galleryImages[nextIndex]);
          }

          function showPreviousImage() {
            const currentIndex = getSelectedImageIndex();

            if (currentIndex === -1) {
              setSelectedImage(galleryImages[0]);
              return;
            }

            const previousIndex =
              (currentIndex - 1 + galleryImages.length) % galleryImages.length;

            setSelectedImage(galleryImages[previousIndex]);
          }

Keyboard navigation:


          document.addEventListener("keydown", (event) => {
            if (lightbox.hidden) {
              return;
            }

            if (event.key === "ArrowRight") {
              showNextImage();
            }

            if (event.key === "ArrowLeft") {
              showPreviousImage();
            }
          });

Pay attention to: Next and previous buttons must always update the image, caption, and alt text together. Handle array boundaries and keyboard navigation intentionally.

4. Ignoring responsive image sizing and performance

Image galleries can become heavy very quickly. If every thumbnail loads a full-size image, the page becomes slow even before the user opens the lightbox. A beginner gallery may look fine with four small demo images, but a real gallery with twelve or twenty high-resolution images can hurt loading speed and mobile performance.

A better implementation separates thumbnail images from full-size images. The gallery grid should load smaller thumbnails, and the lightbox should load the larger version only when the user opens it. Add loading="lazy", define image dimensions or aspect ratio, and use object-fit: cover so the layout does not jump while images load.

Problematic approach:


          <img
            src="/images/full/large-photo-1.jpg"
            alt="Forest path"
          />

          <img
            src="/images/full/large-photo-2.jpg"
            alt="City skyline"
          />

This loads full-size images directly in the gallery grid. The user pays the performance cost even if they never open the lightbox.

Better thumbnail markup:


          <button
            class="gallery__item"
            type="button"
            data-image-id="forest-path"
          >
            <img
              src="/images/thumbs/forest-path.jpg"
              alt="Forest path in soft morning light"
              width="480"
              height="320"
              loading="lazy"
            />
          </button>

Responsive CSS Grid:


          .gallery {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
            gap: 16px;
          }

          .gallery__item {
            border: 0;
            padding: 0;
            border-radius: 16px;
            overflow: hidden;
            cursor: pointer;
            background: transparent;
          }

          .gallery__item img {
            width: 100%;
            aspect-ratio: 4 / 3;
            display: block;
            object-fit: cover;
            transition: transform 180ms ease;
          }

          .gallery__item:hover img,
          .gallery__item:focus-visible img {
            transform: scale(1.04);
          }

Lightbox image:


          .lightbox__panel img {
            max-width: min(92vw, 1100px);
            max-height: 78vh;
            width: auto;
            height: auto;
            object-fit: contain;
          }

Pay attention to: Use thumbnails in the grid and full-size images in the lightbox. Add lazy loading, stable dimensions, responsive grid sizing, and object-fit rules to protect performance and layout stability.

5. Forgetting captions, alt text, and image context

Image galleries often become visually attractive but informationally weak. A common mistake is using empty alt attributes for every image or repeating the same generic phrase such as “gallery image.” If the image is meaningful, the alt text should describe it. If the gallery includes captions, the lightbox should update the caption when the selected image changes.

Captions also help sighted users understand the image. For example, a portfolio gallery may need project names, locations, client categories, or short descriptions. A travel gallery may need place names. A product gallery may need angle descriptions. Without this context, the lightbox becomes only a bigger picture, not a better viewing experience.

Problematic approach:


          <img src="/images/photo-1.jpg" alt="image" />
          <img src="/images/photo-2.jpg" alt="image" />
          <img src="/images/photo-3.jpg" alt="image" />

This alt text does not help users understand what each image shows.

Better image data:


          const galleryImages = [
            {
              id: "old-town",
              thumbnailSrc: "images/thumbs/old-town.jpg",
              fullSrc: "images/full/old-town.jpg",
              alt: "Narrow street in an old European town",
              caption: "Old town street photographed during golden hour"
            },
            {
              id: "desert-road",
              thumbnailSrc: "images/thumbs/desert-road.jpg",
              fullSrc: "images/full/desert-road.jpg",
              alt: "Empty road crossing a desert landscape",
              caption: "Desert road with mountains in the distance"
            }
          ];

Caption update:


          function updateLightboxContent(image) {
            lightboxImage.src = image.fullSrc;
            lightboxImage.alt = image.alt;
            lightboxCaption.textContent = image.caption;
          }

Optional image counter:


          function updateImageCounter() {
            const currentIndex = getSelectedImageIndex();

            imageCounter.textContent = `${currentIndex + 1} of ${galleryImages.length}`;
          }

Pay attention to: Every meaningful image needs useful alt text. Captions, counters, and descriptions make the lightbox more understandable and more professional.

6. Adding a lightbox plugin without understanding its configuration

Using a lightbox library can be a good decision, especially if the gallery needs video support, captions, thumbnails, transitions, mobile gestures, or plugin extensions. The mistake is installing a plugin and treating it as magic. If you do not understand its required markup, initialization, cleanup, and configuration, the gallery may work in one demo but become hard to debug in a real project.

Plugin-based galleries are especially useful when the project includes mixed media such as images and videos. But for a beginner or intermediate portfolio project, you should still understand the basics: which element initializes the gallery, which attributes define full media sources, how captions are passed, and how the plugin behaves on mobile.

Problematic approach:


          lightGallery(document.querySelector(".gallery"));

This may initialize the plugin, but the project does not document what features are enabled, what markup the plugin expects, or how video/caption behavior works.

Better plugin markup:


          <div class="gallery-container" id="gallery-container">
            <a
              href="/images/full/mountain-lake.jpg"
              data-src="images/full/mountain-lake.jpg"
              data-sub-html="<h4>Mountain Lake</h4><p>Morning view near the lake.</p>"
            >
              <img
                src="/images/thumbs/mountain-lake.jpg"
                alt="Mountain lake surrounded by trees"
              />
            </a>
          </div>

Clear initialization:


          const galleryContainer = document.getElementById("gallery-container");

          if (galleryContainer) {
            lightGallery(galleryContainer, {
              speed: 500,
              selector: "a",
              download: false
            });
          }

Document what the plugin handles:


          /*
            Plugin handles:
            - opening and closing the lightbox
            - slide transitions
            - mobile gestures
            - media captions

            Project still controls:
            - image data quality
            - thumbnail performance
            - alt text
            - layout and responsive grid
          */

Pay attention to: A plugin can save time, but it does not replace understanding. Know which features the library handles and which responsibilities still belong to your own gallery code.

After completing this project, you will have a polished intermediate frontend component that demonstrates responsive layout skills, DOM manipulation, interaction logic, modal behavior, transitions, and keyboard support. This is a strong portfolio project because it shows that you can build more than static pages: you can create reusable, user-friendly interface patterns with clean HTML, modern CSS, and vanilla JavaScript. The same concepts will later help you build sliders, product galleries, media viewers, portfolio showcases, and custom modal systems in larger frontend applications.

Reference Implementations Worth Studying

Vanilla JavaScript and CSS Grid reference:
jrrio - Gallery with Lightbox

This is the strongest direct reference for the project because it focuses on a responsive photo gallery with a lightbox using CSS Grid and vanilla JavaScript. The lightbox opens when a user clicks an image from the gallery and can display the selected image together with a URL link and description. This makes it useful for learning the core mechanics without relying on a framework.

Pay particular attention to:

  • How CSS Grid creates the responsive gallery layout.
  • How vanilla JavaScript connects gallery clicks to lightbox behavior.
  • How the lightbox includes more than just an enlarged image by adding a link and description.
  • How a small project can remain understandable when HTML, CSS, and JavaScript responsibilities are separated.
  • What you would improve with keyboard support, focus management, image counters, and stronger accessibility details.

Use this repository as the main implementation baseline. It is close to the target project and demonstrates the essential gallery-to-lightbox flow in a clear, framework-free way.

Simple responsive modal-lightbox reference:
jcamp - Responsive Image Gallery 1

This implementation is useful as a simpler comparison point. It is described as a responsive image gallery with a modal lightbox effect, built around CSS Grid with HTML, CSS, and JavaScript. The project is small, easy to inspect, and helpful for understanding the basic relationship between gallery layout and modal preview behavior.

When studying the code, focus on:

  • How the gallery uses CSS Grid to adapt to different screen widths.
  • How the modal lightbox effect is triggered from gallery items.
  • How a lightweight project can teach the core idea without extra tooling.
  • How JavaScript can stay focused on opening, closing, and changing the active image.
  • Which production details should be added later: alt text, Escape key closing, focus handling, and lazy loading.

Use this repository as the beginner-friendly reference. It is especially helpful if you want to understand the basic mechanics before adding captions, keyboard navigation, or plugin-based features.

Plugin-based image and video gallery reference:
devaduthh - lightGallery CodePen

This CodePen is useful as an alternative direction because it demonstrates a gallery built with the lightGallery JavaScript plugin. The example includes image and video gallery behavior, external resources, plugin initialization, media posters, captions, and support for YouTube, Vimeo, Wistia, and HTML5 video sources.

While reviewing this example, examine:

  • How plugin markup uses data attributes to connect thumbnails with full media sources.
  • How captions can be passed through gallery item attributes.
  • How a plugin can add video-gallery behavior that would take much longer to build manually.
  • How external CSS and JavaScript resources affect setup and project dependencies.
  • Why you still need to understand responsive layout, alt text, performance, and accessibility even when using a gallery library.

Use this example as the advanced/plugin comparison. It is not the best first implementation to copy line by line, but it shows how a simple image lightbox can grow into a richer media gallery.

© 2026 ReadyToDev.Pro. All rights reserved.

Methodology

Privacy Policy

Terms & Conditions