With React, you can easily create a popup system

Demonstrating how to use React to develop a simple, customizable, and accessible popup system.

Existing Systems

There are a lot of popup systems out there, but most of them don’t fit my high standards for user interfaces and development ease.

When I’m putting a popup on a website, I want to make sure that the system is:

  • Simple to use
  • Customisable
  • Accessible

With these requirements, finding a library that has what I need is often tough, and the blocking points are sometimes too uncomfortable to work past.

Requirements

To have a modal system, you must first have a modal root, which is where the system will take place. All we need is a new div#modal-root element in our root document to do this.

This is necessary so that the modal may be styled easily. With a distinct root element, we can be certain that the modal’s parent components do not have styles that will make it more difficult to achieve the perfect style.

We only need to add the correct z-index to the application root and the modal root to ensure that the modal is always on top of the content.

Because we want the readers to behave like the browser, which places the popup on top of everything else, the aria-live area is set to aggressive.

#root {
  position: relative;
  z-index: 1;
}
#modal-root {
  position: relative;
  z-index: 2;
}

<!-- ... -->
<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
  <div id="modal-root" aria-live="assertive"></div>
</body>
<!-- ... -->

Components

The modal component is split into three different components:

  • ModalPortal component that will link our modal to the div#modal-root element
  • ModalView component that aims to handle the visible part of the component
  • ModalAnimated component that will handle the popup domain and the CSS appearance effects of the popup system

The ModalPortal component

The ModalPortal component exists to link our popup to the div#modal-root element that we have created. Here’s the code:

import { useEffect, useRef } from "react";
import { createPortal } from "react-dom";

interface IModalPortalProps {
  active: boolean;
  children: React.ReactNode;
}

export default function ModalPortal({
  active,
  children,
}: IModalPortalProps): React.ReactPortal | null {
  const elRef = useRef<HTMLDivElement>();

  useEffect(() => {
    if (window) {
      elRef.current = window.document.createElement("div");
    }
  }, []);

  useEffect(() => {
    const modalRoot = window.document.getElementById("modal-root");

    if (active && elRef.current && modalRoot !== null) {
      const { current } = elRef;
      modalRoot.appendChild(current);

      return () => {
        modalRoot.removeChild(current);
      };
    }

    return () => {};
  }, [active]);

  if (elRef.current && active) {
    return createPortal(children, elRef.current);
  }

  return null;
}

The ModalView component

This one is basically a layout component so we can style the popup the way we want.

.Overlay {
  background-color: rgba(0, 0, 0, 0.3);
  border: 0;
  height: 100%;
  left: 0;
  padding: 0;
  position: fixed;
  top: 0;
  width: 100%;
  z-index: 1;
}
.Content {
  background: rgba(255, 255, 255, 1);
  border-radius: 16px;
  box-shadow: 0 10px 13px -6px rgba(0, 0, 0, 0.2), 0 20px 31px 3px rgba(0, 0, 0, 0.14),
    0 8px 38px 7px rgba(0, 0, 0, 0.12);
  color: rgba(0, 0, 0, 0.85);
  left: 50%;
  max-height: 80vh;
  max-width: 90vw;
  overflow: auto;
  padding: 32px;
  position: fixed;
  top: 50%;
  transform: translate(-50%);
  width: 600px;
  z-index: 2;
}
.Close {
  background-color: transparent;
  border-radius: 50%;
  border: 0;
  cursor: pointer;
  display: block;
  fill: rgba(0, 0, 0, 1);
  height: 40px;
  margin: -24px -24px 0 auto;
  padding: 8px;
  transition: all 0.15s cubic-bezier(0.4, 0, 0.6, 1);
  width: 40px;
}
.Close:hover {
  background-color: rgba(0, 0, 0, 0.15);
}

import classes from "./ModalView.module.css";

interface IModalViewProps {
  onClose: () => void;
  children: React.ReactNode;
}

const ModalView: React.FC<IModalViewProps> = ({ onClose, children }) => (
  <>
    <button
      className={classes.Overlay}
      onClick={onClose}
      aria-label="Close the popin"
    />

    <div className={classes.Content}>
      <button
        className={classes.Close}
        onClick={onClose}
        aria-label="Close the popin"
      >
        <svg viewBox="0 0 24 24">
          <path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
        </svg>
      </button>
      {children}
    </div>
  </>
);

export default ModalView;

The ModalAnimated component

For the last part of the system, we need a component that will control the modal. Here’s the code:

.ModalAnimated {
  bottom: 0;
  left: 0;
  pointer-events: initial;
  position: absolute;
  right: 0;
  top: 0;
}
.ModalAnimated:global(.modal-enter) {
  opacity: 0;
  transform: translateY(100px) scale(0.9);
}
.ModalAnimated:global(.modal-enter-active) {
  opacity: 1;
  transform: translateY(0) scale(1);
  transition: all cubic-bezier(0, 0, 0.2, 1) 300ms;
}
.ModalAnimated:global(.modal-exit) {
  opacity: 1;
  transform: translateY(0) scale(1);
}
.ModalAnimated:global(.modal-exit-active) {
  opacity: 0;
  transform: translateY(-100px) scale(0.9);
  transition: all cubic-bezier(0, 0, 0.2, 1) 300ms;
}
@media screen and (prefers-reduced-motion: reduce) {
  .ModalAnimated:global(.modal-enter-active) {
    transition: none;
  }
  .ModalAnimated:global(.modal-exit-active) {
    transition: none;
  }
}

import React from "react";
import { CSSTransition } from "react-transition-group";
import classes from "./ModalAnimated.module.css";
import ModalPortal from "./ModalPortal";
import ModalView from "./ModalView";
import useEchap from "./useEchap";

type ViewComponentType = React.FunctionComponent<{
  children: React.ReactNode,
  onClose: () => void,
}>;

interface Props {
  active: boolean;
  children: React.ReactNode;
  onClose: () => void;
  view?: ViewComponentType;
}

const ModalAnimated: React.FC<Props> = ({
  active,
  children,
  onClose,
  view: ViewComponent = ModalView,
}) => {
  useEscape(active, onClose);

  return (
    <CSSTransition in={active} timeout={300} classNames="modal">
      <ModalPortal active={active}>
        <div className={classes.ModalAnimated}>
          <ViewComponent onClose={onClose}>{children}</ViewComponent>
        </div>
      </ModalPortal>
    </CSSTransition>
  );
};

export default ModalAnimated;

The useEscape hook

To improve usability, we can add another great feature to our popup system by adding an escape listener that can close the popup.

To do so, there is a useEscape(active, onClose); code in the ModalAnimated component, but this is yet to be implemented. Here’s the code:

import { useCallback, useEffect } from "react";

export default function useEscape(active: boolean, onClose: () => void) {
  const onEchap = useCallback(
    (event) => {
      if (event.keyCode === 27) {
        onClose();
      }
    },
    [onClose]
  );

  useEffect(() => {
    if (active) {
      window.addEventListener("keydown", onEchap);

      return () => {
        window.removeEventListener("keydown", onEchap);
      };
    }
  }, [onEchap, active]);
}

The usage

The usage is simple: if we want a custom ModalView component, we need the ModalAnimated component with two props.

The popup’s content is simply the children’s elements supplied to ModalAnimated. To keep the page as light as possible, I normally put the content within another component. The code is as follows:

import React, { useState } from "react";
import ModalAnimated from "../components/modal/ModalAnimated";

interface Props {
  [key: string]: never;
}

const PopinUsageSample: React.FC<Props> = () => {
  const [active, setActive] = useState(false);

  return (
    <>
      <ModalAnimated active={active} onClose={() => setActive(false)}>
        Hello, world!
      </ModalAnimated>
      <button onClick={() => setActive(true)}>Open a popin</button>
    </>
  );
};

export default PopinUsageSample;

Conclusion

We can achieve a very modulable and adaptable popup system by using three light components and a simple custom hook.

While there is certainly room for improvement, we have built a system that will please your UI designer and meet the accessibility requirements.

Special thanks to https://hashnode.com/@brunosabot

Total
0
Shares
Leave a Reply

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

Previous Post

Explained: The Top 10 Most Used AWS Services

Next Post

Prepare for Frontend Developer Interviews

Total
0
Share