Add Lightbox to React

March 09, 2024
article featured image
An image library with a lightbox is a nice feature, but not all plug-ins work for our purposes. This effort requires GraphQL to pull images from a folder and create a gallery. React-image-lightbox is outdated, but we can get it working with React 18 and Gatsby 5.

Table of Contents

    §  Growing and Centered Image List

    Image Library

    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%;
      }
    }
    Image Library

    §  Implementing LightBox

    From our initial page, we can import react-image-lightbox and its corresponding CSS.

    import React, { useState } from 'react';
    import MainLayout from 'layouts/MainLayout';
    import { Breadcrumb } from 'react-bootstrap';
    import { graphql } from 'gatsby';
    import { GatsbyImage, getImage, IGatsbyImageData } from 'gatsby-plugin-image';
    import Lightbox from 'react-image-lightbox';
    import ImgColWrapper from 'wrappers/ImageColWrapper';
    import * as LightboxCSS from 'styles/packages/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 (
        <MainLayout>
          <div className="homemade-container-sm mx-auto d-flex flex-column align-items-center">
            <div className="inner-container">
              <Breadcrumb>
                <Breadcrumb.Item href="/">Home</Breadcrumb.Item>
                <Breadcrumb.Item active>Composites</Breadcrumb.Item>
              </Breadcrumb>
              <hr className="m-0" />
              <h1 className="pt-4">Composites</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>
        </MainLayout>
      );
    };
    
    export default CompPage;
    
    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)
              }
            }
          }
        }
      }
    `;
    
    export { Head } from 'components/Head';
    

    The Lightbox defaults still work quite nicely, so when we click on each image, or Lightbox should work quite well.

    Image Library

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