DEV Community

Cover image for React modal with "div"
Ellis
Ellis

Posted on • Edited on

React modal with "div"

The goal

Create a React modal dialog box using the html "div" element. Content is being provided as children. (Compare to: React modal using an html "dialog")

 

Notes

  1. This "Modal" component is generic: the modal content is contained inside the parent container, the modal only wraps it.

  2. In general I strongly suggest you create a modal conditionally like here (see: isModalOpen && below), instead of creating it always but displaying it conditionally. I have seen websites with a large number of modals on their main page, where each modal content was heavy using a significant amount of cpu and memory; on a typical session only a couple of these modals were opened, though the page totally unnecessarily created all of them every time and ended up being very large and slow.

 

Component 1 - the "ModalTester" contains and opens the modal

import { useState } from "react";
import styled from "styled-components";

import { Modal } from ".";

const SomeContentForTheModal = styled.div`
  height: 180px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 20px;
`;

// ------------------------------------
const ModalTester = () => {
  const [isModalOpen, setModalOpen] = useState(false);

  return (
    <div data-testid="ModalTester">
      <button onClick={() => setModalOpen(true)}>Open the modal</button>

      {isModalOpen && (
        <Modal
          title="Modal example"
          proceedButtonText="Proceed"
          onProceed={() => console.log("Proceed clicked")}
          onClose={() => setModalOpen(false)}
        >
          <SomeContentForTheModal>
            <p>This is the modal content.</p>

            <p>To close: click Cancel, press Escape, or click outside.</p>
          </SomeContentForTheModal>
        </Modal>
      )}
    </div>
  );
};

export default ModalTester;
Enter fullscreen mode Exit fullscreen mode

 

Component 2 - the "Modal" component itself

import { ReactNode } from "react";
import styled from "styled-components";

import { Button } from "../components";
import { useKeyDown } from "../hooks";

const Overlay = styled.div`
  z-index: 101;
  position: fixed;
  top: 0px;
  left: 0px;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.6);
`;

const Container = styled.div`
  z-index: 102;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  border-radius: 8px;
  padding: 0;
  background-color: var(--color-whitish);
`;

const TopSection = styled.div`
  position: relative;
  padding: 0 20px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background-color: var(--color-gray-lighter);
  border-radius: 8px 8px 0 0;
`;

const MiddleSection = styled.div`
  position: relative;
  padding: 0 20px;
`;

const BottomSection = styled(TopSection)`
  border-radius: 0 0 8px 8px;
`;

const FlexSpace = styled.div`
  flex-grow: 1;
`;

// ------------------------------------
type Props = {
  title: string;
  proceedButtonText?: string;
  closeButtonText?: string;
  onProceed?: () => void;
  onClose: () => void;
  children: ReactNode;
};

// ------------------------------------
const Modal = ({
  title,
  proceedButtonText,
  closeButtonText,
  onProceed,
  onClose,
  children,
}: Props) => {
  useKeyDown("Escape", onClose);

  const proceedAndClose = () => {
    onProceed?.();
    onClose();
  };

  // Prevents closing when we click inside the modal
  const preventAutoClose = (e: React.MouseEvent) => e.stopPropagation();

  // ------------------------------------
  return (
    <Overlay onClick={onClose}>
      <Container onClick={preventAutoClose}>
        <TopSection>
          <h3>{title}</h3>
        </TopSection>

        <MiddleSection>{children}</MiddleSection>

        <BottomSection>
          {!!proceedButtonText && (
            <Button text={proceedButtonText} onClick={proceedAndClose} />
          )}

          <FlexSpace />

          <Button text={closeButtonText || "Cancel"} onClick={onClose} />
        </BottomSection>
      </Container>
    </Overlay>
  );
};

export default Modal;
Enter fullscreen mode Exit fullscreen mode

 

Additionally a useKeyDown() hook closes the modal if the Escape gets pressed

import { useEffect } from "react";

const useKeyDown = (key: string, onKeyDown: () => void) => {
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === key) onKeyDown();
    };

    window.addEventListener("keydown", handleKeyDown);

    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
};

export default useKeyDown;
Enter fullscreen mode Exit fullscreen mode

 

My recommendation

Personally I clearly prefer this React modal here which uses the html "div", and not the one using the new html "dialog" element. I think:

  • a modal implemented using a "div" is: simpler, more intuitive, safer, and easier to maintain/troubleshoot,
  • the new html "dialog" element is unnecessarily complicated.

 

Thanks for reading. Suggestions/corrections are welcome.

Top comments (0)