How to create a reusable modal component?

How to create a reusable modal component?

Modal has become a ubiquitous UI element in modern web designs. It is a simple yet effective design choice with respect to UX. I believe everyone is well aware of how omnipresent a modal element is the everyday web. Today we will be building a Modal Component in react js and build it in a way that it can be re-used.

Let's look at what we are going to build first.

I’m assuming that you’re familiar with how to create a React app and basic styling with CSS.

Requirments

First, let's set some specific requirements before we can start building the components.

  1. Modal component should be easily resized based on props.
  2. Close modal by outside click.
  3. Close modal on Escape button press.
  4. Modal content should be dynamic as it can be changed easily.

Okay, now we can start building the layout.

Create a basic modal layout

A modal is usually a collection of 4 parts.

  • Overlay: Usually a transparent backdrop container covers the whole screen.
  • Header: Contains modal title and close icon (if any).
  • Body: display the modal content.
  • Footer: display the action buttons. (optional)

Let's start building the components

Note:- I will be using building the component with SCSS and CSS Modules. Feel free to stick with CSS if preferable.

First, we will create two files one for component and one for SCSS.

import styles from "./modal.module.scss";
import { RiCloseFill } from "react-icons/ri";
import classNames from "classnames";

const Modal = ({
  size = "md",                // Width of the modal container
  title,                      // Title of the modal in header part
  isOpen,                     // Modal state (true || false)
  closeOnEsc = true,          // If true the modal can be closed on escape key click
  closeOnOverlayClick = true, // If true modal can be closed on overlay click
  onClose,                    // a function to perform close action for the modal
  overlayClassName,           // Pass classes directly to overlay container
  headerClassName,            // Pass classes directly to header
  children                    // Body elements of the modal
}) => {

    const containerClass = classNames(
        styles.container,
        styles[size],
        overlayClassName
      );
    const headerClass = classNames(styles.header, headerClassName);
    return(
        <div
          className={containerClass}
          tabIndex="-1"
          role="dialog"
          aria-labelledby="exampleModalCenterTitle"
          aria-hidden="true"
        >
          <div className={styles.content}>
            <div className={headerClass}>
              <span className={styles.title}>{title}</span>
              <span onClick={onClose} className={styles.close}>
                <RiCloseFill />
              </span>
            </div>
            {children}
          </div>
        </div>
    )
}

export { Modal }

Here we have declared a few props which will sort of configure the modal component as the requirements specified.

Then we are calculating which classes to put in the container element based on the props given.

Rest is JSX to structure our modal component as per our requirement. Now let's style the component.

Styling

.container {
  position: fixed;
  z-index: 95;
  display: flex;
  justify-content: center;
  align-items: center;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  overflow: auto;
  backdrop-filter: blur(2px);
  background-color: #00000070;
  transition: all 0.3s ease-in-out;
  .content {
      background-color: var(--clr-theme-1);
      margin: auto;
      padding: 1.2rem;
      width: 100%;
      border-radius: 1rem;
      box-shadow: var(--box-shadow-1);
  }
  &.full .content {
      max-width: 100vw;
      min-height: 100vh;
  }
  &.xxl .content {
      max-width: 60rem;
  }
  &.xl .content {
      max-width: 44rem;
  }
  &.lg .content {
      max-width: 38rem;
  }
  &.md .content {
      max-width: 32rem;
  }
  &.sm .content {
      max-width: 26rem;
  }
  &.xs .content {
      max-width: 20rem;
  }

  .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 1rem;
  }
  .title {
      font-size: 500;
      font-weight: 600;
  }
  .body {
      display: flex;
      flex-direction: column;
      overflow-y: auto;
      max-height: 80vh;
  }
  .close {
      color: var(--clr-gray-2);
      background: none;
      font-size: 1.6rem;
      &:hover,
      &:focus {
          color: var(--clr-gray-1);
          background: var(--clr-theme-2);
      }
      &:active {
          transform: scale(0.9);
      }
  }
}
:global(body:not(.dark-mode)) .container {
  --clr-theme-3: var(--clr-gray-3);
}

We have different classes to resize the body of the modal. we can pass the required size through props.

The modal component is somewhat ready to render. let's add it to our App.js file.

import { Modal } from "./Modal/Modal";
import { useState } from "react";

export default function App() {
  const [isModalOpen, setModalState] = useState(false);
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <button onClick={() => setModalState(true)}>Open modal</button>
      <Modal
        size="md"
        isOpen={isModalOpen}
        onClose={() => setModalState(false)}
        title="My Playlists"
      >
        <h1>Hello</h1>
        <p>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Temporibus
          libero ex et ipsum quam itaque illo, modi corrupti aspernatur earum
          cum tempora quae dicta, commodi non nemo hic? Laudantium, odit?
        </p>
    </Modal>
    </div>
  );
}

Add features

Our basic modal component is ready, now let's add some functionality like on Escape click modal should get closed.

const escFunction = useCallback((event) => {
    if (event.keyCode === 27) {
      onClose();
    }
  }, []);

  useEffect(() => {
    closeOnEsc && document.addEventListener("keydown", escFunction);

    return () => {
      closeOnEsc && document.removeEventListener("keydown", escFunction);
    };
  }, [isOpen, closeOnEsc, escFunction]);

Here we are adding an event listener to detect escape key press in useEffect.

Now let's add another feature. as per our requirements, the modal also should get closed on the overlay click. To achieve this we can use a hook called useOnClickOutside. If you want to know why and how this hook works you can refer to this blog

It's pretty simple. the hook takes an element and adds an event listener to check if any click event is triggered outside that element.

const onClickOutsideRef = useOnClickOutside({ handler: onClose });
let contentRef = useRef();
contentRef = closeOnOverlayClick ? onClickOutsideRef : contentRef;

Baes on the closeOnOverlayClick prop we are adding ref to our overlay element and passing onClose function as an argument.

Here we go we have a functional re-usable Modal component. Hope you liked the blog.