Add Lightbox to React

Table of Contents
§ Growing and Centered Image List

The nice thing about using GraphQL is that when we pull our image data, we can pull a small proportional thumbnail and a full size version of the image for when they open the lightbox.
export const portfolioQuery = graphql`
  query CompImages {
    allFile(filter: { relativeDirectory: { eq: "portfolio" } }) {
      edges {
        node {
          id
          childImageSharp {
            thumb: gatsbyImageData(
              width: 175
              height: 175
              placeholder: BLURRED
              formats: [AUTO, WEBP, AVIF]
            )
            full: gatsbyImageData(layout: FULL_WIDTH)
          }
        }
      }
    }
  }
`;The initial div uses Bootstrap's built in styles d-flex and flex-wrap to make the div a Flexbox which wraps. Then when we cycle through each image, we can contain them in a component we'll call ImgColWrapper.
  <div
    className="d-flex flex-wrap"
    style={{ margin: rowMargin + 'px' }}
  >
    {images.map(
      (img: { thumb: IGatsbyImageData }, imgIndex: number) => {
        const thumbImage = getImage(img.thumb);
        if (!thumbImage) {
          return null;
        }
        return (
          <ImgColWrapper
            key={imgIndex}
            gutter={gutter}
            onClick={() => {
              setIsOpen(true);
              setIndex(imgIndex);
            }}
          >
            <GatsbyImage image={thumbImage} alt={`library image`} />
          </ImgColWrapper>
        );
      },
    )}
  </div>The ImgColWrapper component wraps each image in another Flexbox. Each is wrapped in another div with a classname of imageColumn.
// ImageColWrapper.tsx
import React from 'react';
import './ImageColWrapper.scss';
interface ImageColWrapperProps {
  children?: React.ReactNode;
  onClick: () => void;
  gutter: string;
}
const ImageColWrapper = ({
  children,
  onClick,
  gutter,
}: ImageColWrapperProps) => {
  return (
    <div className="imageColumn" onClick={onClick}>
      <div
        className="d-flex flex-grow-0 flex-shrink-0 align-items-center justify-content-center"
        style={{ margin: gutter }}
      >
        {children}
      </div>
    </div>
  );
};
export default ImageColWrapper;
The imageColumn uses these styles to keep each column centered, whether 3, 4, or 5 images are in a row. We use Bootstrap breakpoints to set when the proportions change.
@import 'styles/dominant/bootstrap-custom.scss';
.imageColumn {
  flex-basis: 33%;
  max-width: 33%;
  @include media-breakpoint-up(sm) {
    flex-basis: 25%;
    max-width: 25%;
  }
  @include media-breakpoint-up(lg) {
    flex-basis: 20%;
    max-width: 20%;
  }
}
§ Implementing LightBox
From our initial page, we can import react-image-lightbox and its corresponding CSS.
import React, { useState } from 'react';
import { graphql } from 'gatsby';
import { GatsbyImage, getImage, IGatsbyImageData } from 'gatsby-plugin-image';
import ImgColWrapper from '../wrappers/ImageColWrapper';
import Lightbox from 'react-image-lightbox';
import * as LightboxCSS from '@/styles/lightbox/lightbox.css';
import styled from 'styled-components';
interface CompPageProps {
  data: any;
  gutter: string;
  rowMargin: number;
  lightboxOptions: {};
  onClose: () => void;
}
const StyledLightbox = styled(Lightbox)`
  ${LightboxCSS}
`;
const CompPage: React.FC = ({
  data,
  gutter = '0.25rem',
  rowMargin = 0,
  lightboxOptions = {},
  onClose = () => {},
}: CompPageProps) => {
  const [index, setIndex] = useState(0);
  const [isOpen, setIsOpen] = useState(false);
  const images = data.allFile.edges.map(({ node }) => node.childImageSharp);
  const prevIndex = (index + images.length - 1) % images.length;
  const nextIndex = (index + images.length + 1) % images.length;
  // URLs for full width images
  const mainSrc = images[index]?.full?.images?.fallback?.src;
  const nextSrc = images[nextIndex]?.full?.images?.fallback?.src;
  const prevSrc = images[prevIndex]?.full?.images?.fallback?.src;
  const onCloseLightbox = () => {
    onClose();
    setIsOpen(false);
  };
  return (
    <div>
      <div className="homemade-container-sm mx-auto d-flex flex-column align-items-center">
        <div className="inner-container">
          <hr className="m-0" />
          <h1 className="pt-4">Lightbox Example</h1>
          <div className="pt-3">
            <div
              className="d-flex flex-wrap"
              style={{ margin: rowMargin + 'px' }}
            >
              {images.map(
                (img: { thumb: IGatsbyImageData }, imgIndex: number) => {
                  const thumbImage = getImage(img.thumb);
                  if (!thumbImage) {
                    return null;
                  }
                  return (
                    <ImgColWrapper
                      key={imgIndex}
                      gutter={gutter}
                      onClick={() => {
                        setIsOpen(true);
                        setIndex(imgIndex);
                      }}
                    >
                      <GatsbyImage image={thumbImage} alt={`testing`} />
                    </ImgColWrapper>
                  );
                },
              )}
            </div>
            {isOpen && (
              <Lightbox
                mainSrc={mainSrc || ''}
                nextSrc={nextSrc || ''}
                prevSrc={prevSrc || ''}
                onCloseRequest={onCloseLightbox}
                onMovePrevRequest={() => setIndex(prevIndex)}
                onMoveNextRequest={() => setIndex(nextIndex)}
                imageTitle={images[index].title}
                imageCaption={images[index].caption}
                {...lightboxOptions}
              />
            )}
          </div>
        </div>
      </div>
    </div>
  );
};
export default CompPage;
export const portfolioQuery = graphql`
  query CompImages {
    allFile(filter: { relativeDirectory: { eq: "lightbox" } }) {
      edges {
        node {
          id
          childImageSharp {
            thumb: gatsbyImageData(
              width: 175
              height: 175
              placeholder: BLURRED
              formats: [AUTO, WEBP, AVIF]
            )
            full: gatsbyImageData(layout: FULL_WIDTH)
          }
        }
      }
    }
  }
`;
The Lightbox defaults still work quite nicely, so when we click on each image, or Lightbox should work quite well.

This tutorial is inspired by an implementation of react-image-lighthouse by Brownie Broke.