Skip to main content
Back to Blog
Tutorials
3 min read
November 14, 2024

How to Build an Image Gallery with Lightbox in React

Create a responsive image gallery with lightbox overlay, keyboard navigation, and touch swipe support using React.

Ryel Banfield

Founder & Lead Developer

An image gallery with a lightbox overlay lets visitors browse photos at full size without leaving the page. Here is how to build one from scratch.

Step 1: Define Gallery Data

type GalleryImage = {
  src: string;
  alt: string;
  width: number;
  height: number;
};

const images: GalleryImage[] = [
  { src: "/images/gallery/project-1.jpg", alt: "E-commerce website redesign", width: 1200, height: 800 },
  { src: "/images/gallery/project-2.jpg", alt: "Restaurant mobile app", width: 800, height: 1200 },
  { src: "/images/gallery/project-3.jpg", alt: "SaaS dashboard design", width: 1200, height: 800 },
  { src: "/images/gallery/project-4.jpg", alt: "Real estate listing page", width: 1200, height: 800 },
  { src: "/images/gallery/project-5.jpg", alt: "Fitness app interface", width: 800, height: 1200 },
  { src: "/images/gallery/project-6.jpg", alt: "Corporate website", width: 1200, height: 800 },
];

Step 2: Build the Masonry Grid

import Image from "next/image";

function GalleryGrid({
  images,
  onSelect,
}: {
  images: GalleryImage[];
  onSelect: (index: number) => void;
}) {
  return (
    <div className="columns-1 gap-4 sm:columns-2 lg:columns-3">
      {images.map((image, index) => (
        <button
          key={image.src}
          onClick={() => onSelect(index)}
          className="mb-4 block w-full overflow-hidden rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        >
          <Image
            src={image.src}
            alt={image.alt}
            width={image.width}
            height={image.height}
            className="w-full transition-transform duration-300 hover:scale-105"
            sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
          />
        </button>
      ))}
    </div>
  );
}

Step 3: Build the Lightbox Overlay

"use client";

import { useEffect, useCallback } from "react";
import Image from "next/image";

function Lightbox({
  images,
  currentIndex,
  onClose,
  onPrev,
  onNext,
}: {
  images: GalleryImage[];
  currentIndex: number;
  onClose: () => void;
  onPrev: () => void;
  onNext: () => void;
}) {
  const image = images[currentIndex];

  // Keyboard navigation
  const handleKeyDown = useCallback(
    (e: KeyboardEvent) => {
      if (e.key === "Escape") onClose();
      if (e.key === "ArrowLeft") onPrev();
      if (e.key === "ArrowRight") onNext();
    },
    [onClose, onPrev, onNext]
  );

  useEffect(() => {
    window.addEventListener("keydown", handleKeyDown);
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", handleKeyDown);
      document.body.style.overflow = "";
    };
  }, [handleKeyDown]);

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center bg-black/90"
      role="dialog"
      aria-label="Image lightbox"
      aria-modal="true"
    >
      {/* Close button */}
      <button
        onClick={onClose}
        className="absolute right-4 top-4 z-10 rounded-full bg-white/10 p-2 text-white hover:bg-white/20"
        aria-label="Close lightbox"
      >
        <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
          <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
        </svg>
      </button>

      {/* Previous button */}
      <button
        onClick={onPrev}
        className="absolute left-4 top-1/2 -translate-y-1/2 rounded-full bg-white/10 p-3 text-white hover:bg-white/20"
        aria-label="Previous image"
      >
        <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
          <path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
        </svg>
      </button>

      {/* Image */}
      <div className="relative max-h-[85vh] max-w-[90vw]">
        <Image
          src={image.src}
          alt={image.alt}
          width={image.width}
          height={image.height}
          className="max-h-[85vh] w-auto object-contain"
          priority
        />
      </div>

      {/* Next button */}
      <button
        onClick={onNext}
        className="absolute right-4 top-1/2 -translate-y-1/2 rounded-full bg-white/10 p-3 text-white hover:bg-white/20"
        aria-label="Next image"
      >
        <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
          <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
        </svg>
      </button>

      {/* Caption and counter */}
      <div className="absolute bottom-4 left-1/2 -translate-x-1/2 text-center text-white">
        <p className="text-sm">{image.alt}</p>
        <p className="mt-1 text-xs text-white/60">
          {currentIndex + 1} / {images.length}
        </p>
      </div>
    </div>
  );
}

Step 4: Add Touch Swipe Support

import { useRef } from "react";

function useTouchSwipe(onSwipeLeft: () => void, onSwipeRight: () => void) {
  const touchStartX = useRef(0);
  const touchEndX = useRef(0);

  function handleTouchStart(e: React.TouchEvent) {
    touchStartX.current = e.targetTouches[0].clientX;
  }

  function handleTouchMove(e: React.TouchEvent) {
    touchEndX.current = e.targetTouches[0].clientX;
  }

  function handleTouchEnd() {
    const diff = touchStartX.current - touchEndX.current;
    const threshold = 50;

    if (diff > threshold) {
      onSwipeLeft(); // Swiped left → next
    } else if (diff < -threshold) {
      onSwipeRight(); // Swiped right → prev
    }
  }

  return { handleTouchStart, handleTouchMove, handleTouchEnd };
}

Add the handlers to the lightbox container:

const { handleTouchStart, handleTouchMove, handleTouchEnd } = useTouchSwipe(onNext, onPrev);

<div
  onTouchStart={handleTouchStart}
  onTouchMove={handleTouchMove}
  onTouchEnd={handleTouchEnd}
  className="fixed inset-0 z-50 ..."
>

Step 5: Assemble the Gallery Component

"use client";

import { useState } from "react";

export function ImageGallery({ images }: { images: GalleryImage[] }) {
  const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);

  function handlePrev() {
    setLightboxIndex((prev) =>
      prev !== null ? (prev - 1 + images.length) % images.length : null
    );
  }

  function handleNext() {
    setLightboxIndex((prev) =>
      prev !== null ? (prev + 1) % images.length : null
    );
  }

  return (
    <div>
      <GalleryGrid images={images} onSelect={setLightboxIndex} />
      {lightboxIndex !== null && (
        <Lightbox
          images={images}
          currentIndex={lightboxIndex}
          onClose={() => setLightboxIndex(null)}
          onPrev={handlePrev}
          onNext={handleNext}
        />
      )}
    </div>
  );
}

Step 6: Optimize Performance

  • Use next/image for automatic optimization and lazy loading
  • Provide sizes attribute for responsive image loading
  • Use priority on the lightbox image for faster display
  • Generate thumbnails for the grid and load full-size only in the lightbox
  • Consider blur placeholders with placeholder="blur"

Accessibility

  • Trap focus inside the lightbox when open
  • Support keyboard navigation (Escape, Arrow keys)
  • Use role="dialog" and aria-modal="true"
  • Provide aria-label on all buttons
  • Restore scroll position when closing
  • Ensure all images have descriptive alt text

Need a Portfolio or Gallery Website?

We create image-rich websites for photographers, real estate agents, and creative businesses. Contact us to showcase your work.

Reactimage gallerylightboxNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles