Blog / JavaScript

Modern Lightbox Without Libraries

Learn to create an elegant and fully accessible lightbox in vanilla JavaScript. Keyboard navigation, smooth animations, and zero external dependencies.

Introduction to lightboxes

A lightbox is an interface component that displays images or content overlaid on the page, with a darkened background. It's an essential UX pattern for photo galleries, portfolios and e-commerce sites.

While many libraries exist (Fancybox, Lightgallery, PhotoSwipe...), creating your own lightbox in Vanilla JavaScript offers several advantages:

  • Zero dependency: no external library to load
  • Optimal performance: lightweight and tailored code
  • Total control: unlimited behavior customization
  • Learning: understanding the underlying mechanisms
💡
Good to know

Our final lightbox will be less than 100 lines of JavaScript and will support keyboard navigation, touch swipe, and CSS animations.

Basic HTML structure

Let's start by defining the HTML structure. Our lightbox consists of three main elements: the overlay container, the displayed image, and the navigation controls.

index.html
<!-- Image gallery -->
<div class="gallery">
  <img src="photo-1.jpg" alt="Description 1" data-lightbox/>
  <img src="photo-2.jpg" alt="Description 2" data-lightbox/>
  <img src="photo-3.jpg" alt="Description 3" data-lightbox/>
</div>

<!-- Lightbox container -->
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true">
  <div class="lightbox-content">
    <!-- Close button -->
    <button class="lightbox-close" aria-label="Close">
      <svg width="24" height="24" viewBox="0 0 24 24">
        <line x1="18" y1="6" x2="6" y2="18"/>
        <line x1="6" y1="6" x2="18" y2="18"/>
      </svg>
    </button>

    <!-- Navigation -->
    <button class="lightbox-nav lightbox-prev" aria-label="Previous"></button>
    <button class="lightbox-nav lightbox-next" aria-label="Next"></button>

    <!-- Image -->
    <img class="lightbox-image" id="lightbox-img" src="" alt=""/>

    <!-- Compteur -->
    <div class="lightbox-counter"><span id="current">1</span> / <span id="total">3</span></div>
  </div>
</div>

Important points

  • The data-lightbox attribute identifies clickable images
  • ARIA attributes (role, aria-modal, aria-label) improve accessibility
  • The structure is semantic with <button> for controls

CSS styles and animations

CSS is crucial for creating a smooth experience. We'll use transitions for showing/hiding and transforms for animations.

lightbox.css
/* Background overlay */
.lightbox {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.95);
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;

  /* Entry animation */
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s ease;
}

.lightbox.active {
  opacity: 1;
  visibility: visible;
}

/* Image container */
.lightbox-content {
  position: relative;
  max-width: 90vw;
  max-height: 90vh;

  /* Zoom animation */
  transform: scale(0.9);
  transition: transform 0.3s ease;
}

.lightbox.active .lightbox-content {
  transform: scale(1);
}

/* Main image */
.lightbox-image {
  max-width: 100%;
  max-height: 85vh;
  border-radius: 12px;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
  object-fit: contain;
}

/* Close button */
.lightbox-close {
  position: absolute;
  top: -50px;
  right: 0;
  width: 40px;
  height: 40px;
  background: rgba(255, 255, 255, 0.1);
  border: none;
  border-radius: 50%;
  color: white;
  cursor: pointer;
  transition: all 0.2s ease;
}

.lightbox-close:hover {
  background: #6366f1;
  transform: rotate(90deg);
}

/* Navigation buttons */
.lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 50px;
  height: 50px;
  background: rgba(255, 255, 255, 0.1);
  border: none;
  border-radius: 50%;
  color: white;
  font-size: 24px;
  cursor: pointer;
  transition: all 0.2s ease;
}

.lightbox-prev { left: -70px; }
.lightbox-next { right: -70px; }

.lightbox-nav:hover {
  background: #6366f1;
}

Advanced CSS tips

A few interesting techniques used in this CSS:

  • inset: 0: shorthand for top/right/bottom/left at 0
  • visibility + opacity: enables smooth transitions (display: none cannot be animated)
  • transform: scale(): performant zoom animation (GPU accelerated)

Vanilla JavaScript

The core of our lightbox: the JavaScript that handles opening, closing and navigation between images.

lightbox.js
class Lightbox {
  constructor() {
    this.lightbox = document.getElementById('lightbox');
    this.image = document.getElementById('lightbox-img');
    this.images = [...document.querySelectorAll('[data-lightbox]')];
    this.currentIndex = 0;

    this.init();
  }

  init() {
    // Click on gallery images
    this.images.forEach((img, index) => {
      img.addEventListener('click', () => this.open(index));
      img.style.cursor = 'pointer';
    });

    // Control buttons
    this.lightbox.querySelector('.lightbox-close')
      .addEventListener('click', () => this.close());
    this.lightbox.querySelector('.lightbox-prev')
      .addEventListener('click', () => this.prev());
    this.lightbox.querySelector('.lightbox-next')
      .addEventListener('click', () => this.next());

    // Close by clicking the background
    this.lightbox.addEventListener('click', (e) => {
      if (e.target === this.lightbox) this.close();
    });

    // Keyboard navigation
    document.addEventListener('keydown', (e) => this.handleKeyboard(e));
  }

  open(index) {
    this.currentIndex = index;
    this.updateImage();
    this.lightbox.classList.add('active');
    document.body.style.overflow = 'hidden';
  }

  close() {
    this.lightbox.classList.remove('active');
    document.body.style.overflow = '';
  }

  prev() {
    this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
    this.updateImage();
  }

  next() {
    this.currentIndex = (this.currentIndex + 1) % this.images.length;
    this.updateImage();
  }

  updateImage() {
    const img = this.images[this.currentIndex];
    this.image.src = img.src;
    this.image.alt = img.alt;

    // Update counter
    document.getElementById('current').textContent = this.currentIndex + 1;
    document.getElementById('total').textContent = this.images.length;
  }

  handleKeyboard(e) {
    if (!this.lightbox.classList.contains('active')) return;

    switch (e.key) {
      case 'Escape': this.close(); break;
      case 'ArrowLeft': this.prev(); break;
      case 'ArrowRight': this.next(); break;
    }
  }
}

// Initialisation
new Lightbox();
💡
Class architecture

Using a JavaScript class encapsulates all the logic and facilitates reuse. You can easily have multiple lightbox instances on the same page.

Navigation is a key element of the user experience. Our lightbox supports multiple navigation modes.

Button navigation

The previous/next arrows allow intuitive navigation. Using modulo (%) enables infinite circular navigation.

navigation.js
// Circular navigation
prev() {
  // If index = 0, go to last image
  this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
  this.updateImage();
}

next() {
  // If index = last, wrap back to 0
  this.currentIndex = (this.currentIndex + 1) % this.images.length;
  this.updateImage();
}

Keyboard navigation

Support for arrow keys and Escape is essential for accessibility and ease of use.

keyboard.js
handleKeyboard(e) {
  // Only respond if lightbox is open
  if (!this.lightbox.classList.contains('active')) return;

  switch (e.key) {
    case 'Escape':
      this.close();
      break;
    case 'ArrowLeft':
      e.preventDefault(); // Prevent scrolling
      this.prev();
      break;
    case 'ArrowRight':
      e.preventDefault();
      this.next();
      break;
  }
}

Interactive demo

Here is our lightbox in action. Click on an image to open it, then use the arrows or keyboard to navigate.

⌨️
Keyboard shortcuts

Escape: Close the lightbox
Left arrow: Previous image
Right arrow: Next image

Accessibility and best practices

An accessible lightbox is essential for all users. Here are the key points not to overlook.

ARIA attributes

  • role="dialog": tells screen readers this is a dialog box
  • aria-modal="true": signals that the content behind is inactive
  • aria-label: textual description of buttons

Focus trap

For complete accessibility, you need to "trap" focus within the lightbox when it is open.

focus-trap.js
trapFocus() {
  const focusableElements = this.lightbox.querySelectorAll(
    'button, [href], [tabindex]:not([tabindex="-1"])'
  );

  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  this.lightbox.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;

    if (e.shiftKey && document.activeElement === firstElement) {
      e.preventDefault();
      lastElement.focus();
    } else if (!e.shiftKey && document.activeElement === lastElement) {
      e.preventDefault();
      firstElement.focus();
    }
  });
}
⚠️
Animation and motion

Respect the prefers-reduced-motion preference for users sensitive to animations.

reduced-motion.css
@media (prefers-reduced-motion: reduce) {
  .lightbox,
  .lightbox-content {
    transition: none;
  }
}

Conclusion

You now have a complete and professional lightbox in Vanilla JavaScript. This implementation offers:

  • Zero dependency: no external library required
  • Accessibility: keyboard navigation and ARIA attributes
  • Smooth animations: performant CSS transitions
  • Maintainable code: ES6 class architecture

Don't hesitate to customize the styles and add additional features like zoom, touch swipe or lazy loading of images.

🎨
Go further

Discover our collection of gallery effects in the Effect.Labs library, with animated variations and ready-to-use templates.