DEV Community

Cover image for React modal using html "dialog"
Ellis
Ellis

Posted on • Updated on

 

React modal using html "dialog"

The goal:

  • Create a React modal dialog box using the new html "dialog" element. Content provided as children. (Compare to: React modal using an html "div"
  • Is it possible fully declaratively, without any programmatic javascript calls?
  • Is it better than the usual declarative implementation using React and div's?

w3schools.com says: the "dialog" element makes it easy to create popup dialogs and modals on a web page.

Support: Firefox, Chrome, Edge, Safari.

Notes:

  • The "Modal" component is generic, it shows content from the parent container, provided as children.
  • onCancel() is necessary for resetting the state variable "open" when Escape is pressed.
  • preventAutoClose() prevents closing when we click inside the modal dialog box.

I created two components:

First, the "DialogModalTester" component contains and opens the modal:

import { useState } from "react";

// @ts-ignore
import { DialogModal } from "components";

const DialogModalTester = () => {
  const [isOpened, setIsOpened] = useState(false);

  const onProceed = () => {
    console.log("Proceed clicked");
  };

  return (
    <div>
      <button onClick={() => setIsOpened(true)}>Open "dialog" modal</button>

      <DialogModal
        title="Dialog modal example"
        isOpened={isOpened}
        onProceed={onProceed}
        onClose={() => setIsOpened(false)}
      >
        <p>To close: click Close, press Escape, or click outside.</p>
      </DialogModal>
    </div>
  );
};

export default DialogModalTester;
Enter fullscreen mode Exit fullscreen mode

Secondly, the "DialogModal" component itself:

import { useEffect, useRef } from "react";
import styled from "styled-components";

const Container: any = styled.dialog`
  width: 400px;
  border-radius: 8px;
  border: 1px solid #888;

  ::backdrop {
    background: rgba(0, 0, 0, 0.3);
  }
`;

const Buttons = styled.div`
  display: flex;
  gap: 20px;
`;

type Props = {
  title: string;
  isOpened: boolean;
  onProceed: () => void;
  onClose: () => void;
  children: React.ReactNode;
};

const DialogModal = ({
  title,
  isOpened,
  onProceed,
  onClose,
  children,
}: Props) => {
  const ref: any = useRef(null);

  useEffect(() => {
    if (isOpened) {
      ref.current?.showModal();
      document.body.classList.add("modal-open"); // prevent bg scroll
    } else {
      ref.current?.close();
      document.body.classList.remove("modal-open");
    }
  }, [isOpened]);

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

  const preventAutoClose = (e: React.MouseEvent) => e.stopPropagation();

  return (
    <Container ref={ref} onCancel={onClose} onClick={onClose}>
      <div onClick={preventAutoClose}>
        <h3>{title}</h3>

        {children}

        <Buttons>
          <button onClick={proceedAndClose}>Proceed</button>
          <button onClick={onClose}>Close</button>
        </Buttons>
      </div>
    </Container>
  );
};

export default DialogModal;
Enter fullscreen mode Exit fullscreen mode

As a nice-to-have, add the following class to your global css to prevent the body from scrolling when the modal is open. In browser developer tools you can observe this class actually being added and removed.

body.modal-open {
  overflow: hidden; /* see "modal-open" in Modal.tsx */
}
Enter fullscreen mode Exit fullscreen mode

Conclusion:

  • Is it better than the typical declarative implementation using React and div's? YES, this seems simpler and shorter, though the need for programmatic javascript calls makes it still not too simple with React.
  • Is it possible fully declaratively, without programmatic javascript calls? NO, unfortunately we need useRef and programmatic javascript calls if we want a modal dialog with a backdrop.

Thanks for reading. Suggestions/corrections are welcome.

Top comments (5)

Collapse
 
demondragong profile image
Gaël de Mondragon

There are not many examples of React modals using the HTMLDialogElement and even less with such a clear and clean implementation.
Thank you very much!

Collapse
 
demondragong profile image
Gaël de Mondragon

I made it into an npm package for a training project: npmjs.com/package/react-basic-moda...

Collapse
 
elsyng profile image
Ellis

Great. The npm page looks neat. I'll try it when I have time :)

Collapse
 
shimbarks profile image
Shimbarks • Edited

Thanks for this well detailed yet concise guide!

A few comments though:

  1. If you use the keyboard you'll find out there's a bug: after closing the dialog with the Esc key, the dialog won't open again. That's because the isOpened prop isn't being updated upon Esc, hence clicking the Open "dialog" modal button doesn't change the state and the useEffect isn't being called. In order to fix this you need to listen to keyboard events on the window/document (not on the dialog itself, otherwise it won't work if the Esc key is pressed after shifting the focus from the dialog element) and call onClose after Esc was pressed.

  2. Just a minor tedious TypeScript suggestion: replace const ref: any = useRef(null) with const ref = useRef<HTMLDialogElement>(null).

  3. Not really a comment but a question: why do we need the preventAutoClose stuff? I tried your code without it and I haven't encountered the situation in which clicking the dialog content closes it. What am I missing?

Thanks!

Collapse
 
elsyng profile image
Ellis • Edited

Thanks.

Point 1. Just tested with Firefox (v112) and Edge (v111), and I'm afraid I haven't been able to reproduce that. (isOpened "is" being updated properly.)

Point 2. If I do that, I get

TS2339: Property 'showModal' does not exist on type 'HTMLDialogElement'.
  > 38 |       ref.current?.showModal();
       |                    ^^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

Point 3. Again on Firefox & Edge, if I remove preventAutoClose() and click inside the dialog, it closes.

Which perhaps goes to show, that the various implementations of the <Dialog> html element may have some bugs or inconsistencies as yet.

Visualizing Promises and Async/Await 🤓

async await

☝️ Check out this all-time classic DEV post on visualizing Promises and Async/Await 🤓