Create a React Custom Lightbox Gallery

With many lightbox gallery solutions on NPM, I ended up writing my own React Custom Lightbox Gallery Component to meet my specific needs.

I was looking for a “lightbox” type gallery that would allow me to display images (multiple images in the same modal), videos, text, and a mix of that, all within the same modal window. All I could find were galleries that could only display a single image in a modal.

This is a two-part tutorial:

  1. Create a React Custom Modal
  2. Create a React Custom Lightbox Gallery (you are here)

[topads][/topads]

React Custom Lightbox Gallery Preview

React Custom Lightbox Gallery
React Custom Lightbox Gallery

A little context about this gallery before we start: I created this lightbox gallery for my wife’s website (an artist, an illustrator, and a graphic designer), so some of the variables reflect a “portfolio” context. That being said, let us start.

The following will be the content structure for this tutorial:

  1. Gallery images/content structure. I’m storing the images data in a JSON file
  2. A parent container passing the images data to the gallery
  3. A Gallery component looping through the images data forming the gallery and modal
  4. Helper Components

Images Content Structure

Note: I’m processing all of my images and videos through Cloudinary, so the URLs here will be relative to my Cloudinary account.

art.json file:

[
  {
    "thumb": "portfolio/art/baby-watercolor-portrait-andrea-silva-1.jpg",
    "title": "Aedan's Milk Bath",
    "blurb": "11x14 on watercolor paper.",
    "portfolioType": "image",
    "portfolio": [
      "portfolio/art/baby-watercolor-portrait-andrea-silva-1.jpg",
      "portfolio/art/baby-watercolor-portrait-andrea-silva-2.jpg",
      "portfolio/art/baby-watercolor-portrait-andrea-silva-3.jpg"
    ]
  },
  {
    "thumb": "portfolio/motion-graphics/february-budget-monster-motion-graphics-andrea-silva-1.jpg",
    "title": "February Budget Monster",
    "blurb": "To increase an awareness ... to promote giving.",
    "portfolioType": "video",
    "portfolio": [
      "portfolio/motion-graphics/february-budget-monster-motion-graphics-andrea-silva-1.jpg|portfolio/motion-graphics/february-budget-monster-motion-graphics-andrea-silva.mp4"
    ]
  }
]
  • thumb: Gallery thumbnail.
  • title: This is the gallery thumbnail image’s alt & title attributes and the modal’s heading.
  • blurb: Whatever text I want to display in the modal. My use case is the image/video description.
  • portfolioType: I support two portfolio (or lightbox) types. Meaning, when I click on a gallery thumbnail image, the modal will display either an array of images or videos.
    • As of now, I only support either only images or videos for a particular JSON node, but it wouldn’t be hard to extend it to support mixed media.
  • portfolio: Array of images or videos to be displayed in the modal.
    • Notice the video array item is a little different than the image array item. Here I have two relative URLs separated by a pipe (|). The first part will be the video’s poster image and the second the actual video source.

The Container to the React Lightbox Gallery

art.js component:

import React from 'react';

import { MainLayout } from '~components/layouts/MainLayout';
import { SecondaryLayout } from '~components/layouts/SecondaryLayout';
import { Title } from '~styles/Title';

// Here we import the Lightbox Gallery Component
import { Gallery } from '~components/Gallery/Gallery';

// Here we import the images data content from the previous section
import artData from '~content/portfolio/art.json';

const Art = () => {
  return (
    <MainLayout pageTitle="Fine Art" pathName="portfolio/art">
      <SecondaryLayout>
        <Title>Fine Art</Title>
        <p>
          As a fine art and portrait artist, my primary medium is watercolor,
          but I also enjoy painting with oils. My inspiration comes from nature,
          travel, and the everyday moments that make life special.
        </p>
      </SecondaryLayout>

      {/* The lightbox gallery component acepts only a "data" prop for the
      images content. */}
      <Gallery data={artData} />

    </MainLayout>
  );
};

export default Art;

The React Custom Lightbox Gallery Component

As you saw on the preview GIF, the gallery has a masonry layout, for which I implement react-masonry-css package.

Let’s start with a “skeleton” template, Gallery.js component:

import React, { useState, useReducer } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';

// The masonry layout gallery
import Masonry from 'react-masonry-css';

// Helper component for my images in Cloudinary
import { Image } from '~helpers/Image';

// Utility to pass Cloudinary custom transformations to the above Image component
import { transformationsFormat } from '~utils/index';

// SVG magnifier icon at the center of a gallery image component when hovered over.
// Refer to the preview GIF
import { ZoomIn } from '~svgs/ZoomIn';

// Custom modal component from the first tutorial in the series
import { Modal } from '~src/components/Gallery/Modal';

const Gallery = ({ data }) => {

  return ();
};

// The data prop is an array of objects reflecting a JSON node from the
// images data structure (first section)
Gallery.propTypes = {
  data: PropTypes.arrayOf(
    PropTypes.shape({
      thumb: PropTypes.string.isRequired,
      title: PropTypes.string.isRequired,
      blurb: PropTypes.string,
      portfolio: PropTypes.arrayOf(PropTypes.string).isRequired,
    }),
  ).isRequired,
};

export { Gallery };

CSS

Even though the CSS here is specific to this component, some other global styles are being applied.

react-masonry-css.css. These styles are necessary to implement react-mansory-css package:

.my-masonry-grid {
  display: flex;
  margin-left: 0; /* gutter size offset */
  width: auto;
}

.my-masonry-grid_column {
  padding-left: 0; /* gutter size */
  background-clip: padding-box;
}

Now import the previous styles and create a new styled component.

Gallery.js compnent:

import '~styles/react-masonry-css.css';

// This will be wrapping the individual images
const GalleryImageContainer = styled.a`
  margin-bottom: -0.7rem;
  display: block;

  /* Since the SVG icon needs to be contained within the individual image,
  I need the wrapper to be relative positioned  */
  position: relative;

  /* By default, the SVG icon will have an opacity of 0 (not visible) */
  &:hover svg {
    opacity: 1;
  }

  /* This gives the individual image a darkening look when hovered over */
  &:hover img {
    filter: brightness(50%);
  }

  img {
    margin-bottom: 0;

    /* Animation for a smooth filter transition */
    transition: filter 0.2s ease-in;

    padding: 0.7rem;
    width: 100%;
    height: 100%;
  }

  /* The SVG icon will display at the center of the individual image */
  svg {
    margin: auto;
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    opacity: 0;

    /* Animation for a smooth opacity transition */
    transition: opacity 0.2s ease-in;
  }
`;

Masonry Layout

I’m only including the component’s return method below

Gallery.js component:

import '~styles/react-masonry-css.css';

const GalleryImageContainer = styled.a`...`

const Gallery = ({ data }) => {
  return (
    <>
      <Masonry
        // Different columns can be specified by passing an object containing the 
        // window widths as keys and the number of columns as their value. 
        // To have a fallback value, use the default key.
        breakpointCols={{ default: 5, 1186: 4, 910: 3, 700: 2, 500: 1 }}

        // The below two classes come from the css file we created earlier: 
        // react-masonry-css.css
        className="my-masonry-grid"
        columnClassName="my-masonry-grid_column"
      >
        {/* We loop thru all the images, creating a link (GalleryImageContainer)
        for each one. */}
        {data.map(
          ({ thumb, title, blurb, portfolioType, portfolio }, index) => (
            <GalleryImageContainer
              key={thumb}
              href="#"
            >
              {/* This is my custom Image component to handle Cloudinary images. 
              All it does is put together the relative image link path, applies
              the transformations, and returns an img element */}
              <Image
                relativePath={thumb}
                alt={title}
                title={title}
                transformations={transformationsFormat('w_300')}
              />

              {/* This is the SVG icon that appears when hovered over the image */}
              <ZoomIn />
            </GalleryImageContainer>
          ),
        )}
      </Masonry>
    </>
  );
};

I will go over the Image component later in the tutorial, but for now, this is how the gallery looks so far:

React Custom Lightbox Gallery - Masonry Layout
React Custom Lightbox Gallery – Masonry Layout

Lightbox Modal State

When we click on one of the images within the gallery, a lightbox modal opens (as seen in the preview GIF earlier) and we can see the heading, description, and images/videos corresponding to that image. When we click on the left/right arrow button (or press the left/right arrow key), we go to the previous/next image.

All of that data is being saved in the local state and switched accordingly when we change the image. I’m implementing useReducer hook to manage this local state.

Building on top of the previous code snippet, the updated code will be highlighted.

Gallery.js component:

// This is what we see in the modal, with the exception of currentIndex, and corresponds
// to a single node from the initial images JSON data.
// "currentIndex" is used to identify the position in the image data array and calculate
// the previous/next position when we change slides
const initialModalBodyState = {
  heading: '',
  blurb: '',
  portfolioType: '',
  portfolio: [],
  currentIndex: 0,
};

// This is the reducer for useReducer hook. Since I don't need the first
// parameter, (current state) in the reducer, I replace it with an underscore.
// I destructure the second parameter (action) and just straight replace/update
// the current state by returning a new object matching the state (initialModalBodyState)
const modalBodyReducer = (
  _,
  { heading, blurb, portfolioType, portfolio, currentIndex },
) => ({
  heading,
  blurb,
  portfolioType,
  portfolio,
  currentIndex,
});

// These two constants are being used to move to the previous/next slide.
// I'm also exporting them because I use them in the Modal component.
// I will point them out later
export const FORWARD = 'forward';
export const BACKWARD = 'backward';

const Gallery = ({ data }) => {
  // useReducer hook declaration. Again, this state is used to display what you see
  // in the Modal. a.k.a The Modal Body
  const [modalBodyState, dispatchModalBodyState] = useReducer(
    modalBodyReducer,
    initialModalBodyState,
  );

  // We call this method when we click on one of the gallery images to set the state,
  // or current selected image, passing all the necessary data for the Modal component.
  const setModalBodyState = (
    heading,
    blurb,
    portfolioType,
    portfolio,
    index,
  ) => {
    // This dispatch method sends the new state to the modalBodyReducer method from 
    // useReducer
    dispatchModalBodyState({
      heading,
      blurb,
      portfolioType,
      portfolio,
      currentIndex: index,
    });
  };

  // Once we have the Modal open, we need a way to change the slide. We pass a direction
  // variable, this would be either the "FORWARD" or "BACKWARD" constant from earlier.  
  const changeSlide = direction => {
    // We need to know the current image position in the array, so we read the
    // "currentIndex" from state
    const currentIndex = modalBodyState.currentIndex;

    // With the "calculateNextSlide" method we figure out the next image we need to show
    // in the modal, passing the direction, the current position and the entire array
    // data. This method returns the next slide node and the "next index" which becomes
    // the current index
    const { nextSlide, nextIndex } = calculateNextSlide(
      direction,
      currentIndex,
      data,
    );

    // Finally we set the state with the next, or new current, gallery image
    dispatchModalBodyState({
      heading: nextSlide.title,
      blurb: nextSlide.blurb,
      portfolioType: nextSlide.portfolioType,
      portfolio: nextSlide.portfolio,
      currentIndex: nextIndex,
    });
  };

  return (
    <>
      <Masonry
        breakpointCols={{ default: 5, 1186: 4, 910: 3, 700: 2, 500: 1 }}
        className="my-masonry-grid"
        columnClassName="my-masonry-grid_column"
      >
        {data.map(
          ({ thumb, title, blurb, portfolioType, portfolio }, index) => (
            <GalleryImageContainer
              key={thumb}
              href="#"

              // When we click on a gallery image, we call "setModalBodyState" method
              // we saw earlier to set the state to the selected image
              onClick={e =>
                setModalBodyState(title, blurb, portfolioType, portfolio, index)
              }
            >
              <Image
                relativePath={thumb}
                alt={title}
                title={title}
                transformations={transformationsFormat('w_300')}
              />
              <ZoomIn />
            </GalleryImageContainer>
          ),
        )}
      </Masonry>
    </>
  );
};

// We called this method earlier to calculate the next slide
const calculateNextSlide = (direction, currentIndex, data) => {
  let nextIndex;

  // Simple switch statement doing the "heavy lifting" to calculate whether we
  // are requesting the previous or next slide in the image array data
  switch (direction) {
    case FORWARD:
      // Add 1 to the current index
      nextIndex = currentIndex + 1;

      // If the next index is greater than the image array data size, then we
      // want to show the first image, so we assign 0 as the next index. Basically
      // looping the array forward
      if (nextIndex > data.length - 1) nextIndex = 0;

      break;
    case BACKWARD:
      // Subtract 1 to current index
      nextIndex = currentIndex - 1;

      // If the next index is -1, we want to show the last image in the image
      // array data so we assign the last array index. Basically looping backwards
      if (nextIndex === -1) nextIndex = data.length - 1;

      break;
    default:
      nextIndex = 0;
      break;
  }

  // Finally we return the next index and the next slide
  return {
    nextIndex,
    nextSlide: data[nextIndex],
  };
};

The Modal

I will not go into detail about how the modal component works as I went through it in the first part of this series. If you haven’t read it I would recommend reading that post first: Create a React Custom Modal

Now that we know which image to display in the modal, let’s import the component and implement it.

Updated code is highlighted and some code has been redacted.

Gallery.js component:

import { Modal } from '~src/components/Gallery/Modal';

const Gallery = ({ data }) => {
  // We want to save the state of the modal, whether it is opened or closed
  const [isModalOpen, setIsModalOpen] = useState(false);

  const [modalBodyState, dispatchModalBodyState] = useReducer(
    modalBodyReducer,
    initialModalBodyState,
  );

  // Method to toggle the modal state
  const toggleModal = () => setIsModalOpen(!isModalOpen);

  const setModalBodyState = (...) => {...};

  const changeSlide = direction => {...};

  return (
    <>
      <Masonry
        breakpointCols={{ default: 5, 1186: 4, 910: 3, 700: 2, 500: 1 }}
        className="my-masonry-grid"
        columnClassName="my-masonry-grid_column"
      >
        {data.map(
          ({ thumb, title, blurb, portfolioType, portfolio }, index) => (
            <GalleryImageContainer
              key={thumb}
              href="#"
              onClick={e =>
                setModalBodyState(title, blurb, portfolioType, portfolio, index)
              }
            >
              <Image
                relativePath={thumb}
                alt={title}
                title={title}
                transformations={transformationsFormat('w_300')}
              />
              <ZoomIn />
            </GalleryImageContainer>
          ),
        )}
      </Masonry>
      
      {/* Show the modal when the modal state is true  */}
      {isModalOpen && (
        <Modal
          // We pass the modal state and the toggle method
          modalState={{ value: isModalOpen, toggle: toggleModal }}

          // We also need changeSlide method to change the image when the 
          // left/right arrow buttons or keys are pressed
          changeSlide={changeSlide}
        >
          {/* We pass the entire state object as "children" holding the data we want
          to show in the modal: heading, blurb, portfolioType, and portfolio, */}
          {modalBodyState}
        </Modal>
      )}

    </>
  );
};

const calculateNextSlide = (direction, currentIndex, data) => {...};

Modal.js component:

import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';

// Package to allow keyboard shortcuts
import tinykeys from 'tinykeys';

// Package to enable swiping backward/forward on mobile devices
import Swipe from 'react-easy-swipe';

// Custom helper that returns either an "Image" or "Video" component
// depending on the "PortfolioType" for each node in the data array
// displaying all the images/videos in the node
import { PortfolioSelector } from '~helpers/PortfolioSelector';

// SVG components for the backward/forward arrow icons. 
import { ArrowLeft, ArrowRight } from '~svgs/ChevronCircle';

// We created these constants earlier in "Gallery.js"
import { FORWARD, BACKWARD } from '~components/Gallery/Gallery';

// Modal styles
import {
  Container as ModalContainer,
  Body,
  Close as CloseModal,
} from '~styles/Modal';

//#region Styles

// Styles for the Modal body. The body will display either images
// or videos, so we have the styling for both tags here
const ModalBody = styled(Body)`
  display: flex;
  flex-direction: column;
  z-index: 1000;
  img {
    align-self: center;
  }
  video {
    width: 100%;
  }
  @media (min-width: '824px') {
    padding: 2rem;
  }
`;

// Base styles for the button that will wrap the left/right SVG arrows
// (refer to the Preview GIF). First we remove all the "default" style
// for the button; then we absolute position to the left and 45% from
// the top of the screen.
const ArrowButtonBase = styled.button`
  padding: 0;
  border: 0;
  background: none;
  outline: none;
  cursor: pointer;
  position: absolute;
  top: 45%;
`;

// We add additional specific styles for the left and right arrows.
const ArrowButtonLeft = styled(ArrowButtonBase)`
  margin-left: 2rem;
`;
const ArrowButtonRight = styled(ArrowButtonBase)`
  right: 0;
  margin-right: 2rem;
`;

//#endregion

// Destructuring the props, we get: children, modalState, changeSlide.
// Refer to the PropType definitions at the end of the component
const Modal = ({ children, modalState, changeSlide }) => {
  const { heading, blurb, portfolioType, portfolio } = children;
  const chevronColor = '#474747';

  // We call "changeSlide" method when we want to move to the previous
  // or next image in the gallery
  const forward = () => changeSlide(FORWARD);
  const backward = () => changeSlide(BACKWARD);

  useEffect(() => {
    let unsubscribe = tinykeys(window, {
      // ESC key to close the modal
      Escape: () => modalState.toggle(),

      // Left/Right arrow keys to switch slides
      ArrowLeft: () => backward(),
      ArrowRight: () => forward(),
    });

    return () => {
      unsubscribe();
    };
  });

  return (
    <ModalContainer isOpen={modalState.value}>
      {/* Left arrow button that calls "changeSlide" to move backwards */}
      <ArrowButtonLeft
        aria-label="Go To Previous Slide"
        name="Go To Previous Slide"
        onClick={e => backward()}
      >
        {/* SVG icon that takes in a color for the chevron figure */}
        <ArrowLeft pathFill={chevronColor} />
      </ArrowButtonLeft>

      {/* We need to wrap the Modal, or element we want to make swipeable, and pass in
      the methods that will move the slide forward and backward. Tolerance is to prevent
      accidental swipes, so begin swiping after X pixels. */}
      <Swipe onSwipeLeft={forward} onSwipeRight={backward} tolerance={100}>
        <ModalBody>
          <CloseModal onClick={modalState.toggle}>×</CloseModal>
          <h1>{heading}</h1>
          <p>{blurb}</p>

          {/* This helper component takes in 
           - portfolioType: whether the modal will have images or videos.
           - portfolio: the array or images/videos to display.
           - heading: used in the "alt" and "title" attributes for the images */}
          <PortfolioSelector
            type={portfolioType}
            portfolio={portfolio}
            heading={heading}
          />

        </ModalBody>
      </Swipe>

      {/* Same as above */}
      <ArrowButtonRight
        aria-label="Go To Next Slide"
        name="Go To Next Slide"
        onClick={e => forward()}
      >
        <ArrowRight pathFill={chevronColor} />
      </ArrowButtonRight>
    </ModalContainer>
  );
};

Modal.defaultProps = {
  children: {
    heading: '',
    blurb: '',
    portfolioType: '',
    portfolio: [],
    currentIndex: 0,
  },
};

Modal.propTypes = {
  children: PropTypes.shape({
    heading: PropTypes.string.isRequired,
    blurb: PropTypes.string,
    portfolioType: PropTypes.string.isRequired,
    portfolio: PropTypes.arrayOf(PropTypes.string).isRequired,
    currentIndex: PropTypes.number,
  }).isRequired,
  modalState: PropTypes.shape({
    value: PropTypes.bool.isRequired,
    toggle: PropTypes.func.isRequired,
  }).isRequired,
  changeSlide: PropTypes.func.isRequired,
};

export { Modal };

Helper Components

PortfolioSelector.js component:

import React from 'react';
import PropTypes from 'prop-types';

// We saw these earlier in the tutorial
import { Image } from '~helpers/Image';
import { transformationsFormat } from '~utils/index';

// Helper component that returns a "video" element
import { Video } from '~helpers/Video';

const PortfolioSelector = ({ type, portfolio, heading }) => {
  return (
    <>
      {/* We map over the array of images/videos returning either an "img", 
      "video" or "div" element */}
      {portfolio.map((relativePath, index) => {
        switch (type) {
          case 'image':
            return (
              <Image
                key={relativePath}
                relativePath={relativePath}
                alt={`${heading} ${index + 1}`}
                title={`${heading} ${index + 1}`}
                transformations={transformationsFormat('w_1000')}
              />
            );
          case 'video':
            // When the "portfolioType" is video, we only need the relative path. Remember,
            // the path is a little different than the image path. We have two relative 
            // URLs separated by a pipe (|). The first part will be the video poster and
            // the second the actual video source.
            return <Video key={relativePath} relativePath={relativePath} />;
          default:
            // In case we misspelled, or put another string in the "portfolioType" field,
            // we just return an "error" notice.
            return (
              <div
                key={relativePath}
                style={{ background: '#EA868F', fontWeight: 'bold' }}
              >
                <p>Check the content source to have a valid portfolio type.</p>
                <p>Valid portfolio types:</p>
                <ul>
                  <li>image</li>
                  <li>video</li>
                </ul>
              </div>
            );
        }
      })}
    </>
  );
};

PortfolioSelector.propTypes = {
  type: PropTypes.string.isRequired,
  portfolio: PropTypes.arrayOf(PropTypes.string).isRequired,
  heading: PropTypes.string.isRequired,
};

export { PortfolioSelector };

Image.js component:

import React from 'react';
import PropTypes from 'prop-types';

// Package to lazy load images
import 'lazysizes';

const Image = ({ alt, title, relativePath, transformations }) => {
  return (
    // We form the image URL and apply any additional Cloudinary transformations
    <img
      src={`https://res.cloudinary.com/${process.env.GATSBY_CLOUDINARY_USER}/image/upload/e_blur:1500,f_auto,q_40${transformations}/andrea-silva-design/${relativePath}`}
      alt={alt}
      title={title}

      // The following three attributes are for lazysizes' API
      data-sizes="auto"
      data-src={`https://res.cloudinary.com/${process.env.GATSBY_CLOUDINARY_USER}/image/upload/f_auto,q_auto${transformations}/andrea-silva-design/${relativePath}`}
      className="lazyload"
    />
  );
};

Image.propTypes = {
  alt: PropTypes.string.isRequired,
  title: PropTypes.string,
  relativePath: PropTypes.string.isRequired,
  transformations: PropTypes.string,
};

Image.defaultProps = {
  title: '',
  transformations: '',
};

export { Image };

utils/index.js utilities:

// Simple utility method to add a comma at the beginning of a string
export const transformationsFormat = transformations => `,${transformations}`;

Video.js component:

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';

// We destructure the props:
// - relativePath: We have two relative 
//    URLs separated by a pipe (|). The first part will be the video poster and
//    the second the actual video source.
// - showNoSupport: the "video" tag has an option to show a "no support" message.
//    Pass true/false whether you want to show a message or not
// - attributes: any additional video tag attributes
// - transformations: Cloudinary transformations for the poster image
const Video = ({
  relativePath,
  showNoSupport,
  attributes,
  transformations,
}) => {
  // We are storing the relative path for the poster image (thumb) and video 
  // source in state
  const [sources, setSources] = useState({ thumb: '', video: '' });

  useEffect(() => {
    // First we split the combined paths by a pipe
    const sources = relativePath.split('|');

    // Then we update the state. The first element is the poster image,
    // the second element is the video source
    setSources({
      thumb: sources[0],
      video: sources[1],
    });
  }, [relativePath]);

  if (sources.video === '') return null;

  return (
    <video
      {...attributes}
      poster={`https://res.cloudinary.com/${process.env.GATSBY_CLOUDINARY_USER}/image/upload/f_auto,q_60,w_500/andrea-silva-design/${sources.thumb}`}
    >
      <source
        type="video/mp4"
        src={`https://res.cloudinary.com/${process.env.GATSBY_CLOUDINARY_USER}/video/upload/f_auto${transformations}/andrea-silva-design/${sources.video}`}
      />
      {showNoSupport ? (
        <p>
          Sorry, your browser doesn't support embedded videos.
          <br />
          Here is a{' '}
          <a
            href={`https://res.cloudinary.com/${process.env.GATSBY_CLOUDINARY_USER}/video/upload/andrea-silva-design/${sources.video}`}
          >
            link to the video
          </a>{' '}
          instead.
        </p>
      ) : null}
    </video>
  );
};


// The only required prop is the "relativePath"
Video.propTypes = {
  relativePath: PropTypes.string.isRequired,
  showNoSupport: PropTypes.bool,
  attributes: PropTypes.object,
  transformations: PropTypes.string,
};

// Defaults
Video.defaultProps = {
  showNoSupport: true,
  attributes: {
    controls: true,
    disablePictureInPicture: true,
    controlsList: 'nodownload',
  },
  transformations: '',
};

export { Video };

In conclusion

I hope you enjoyed this two-part tutorial as much as I did putting it together.

You can have a look at the final product on my wife’s website: https://andreasilva.design/portfolio/art.

And the React Custom Lightbox Gallery source code here: https://github.com/esausilva/andrea-silva-design/tree/master/src/components/Gallery.

Let me know your thoughts in the comments!

Final Thoughts

This is a very crude implementation and there are many ways to further extract some of the components to create a better API overall. However, this is working for my use case and can serve as a base for you to improve upon it.

If you liked this short tutorial, share it on your social media and you can follow me on Twitter or LinkedIn.


Consider giving back by getting me a coffee (or a couple) by clicking the following button:

[bottomads][/bottomads]

Spread the love

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.