DEV Community

Cover image for How to do a Modal in React: the HTML first approach
Romain Guillemot
Romain Guillemot

Posted on • Edited on • Originally published at rocambille.github.io

5 3

How to do a Modal in React: the HTML first approach

Do HTML before doing CSS, or JS... or React.

First, there was a modal

This story started with a modal. I needed a modal window in a React project. As a recall, here is a good definition from wikipedia:

A modal window creates a mode that disables the main window but keeps it visible, with the modal window as a child window in front of it. Users must interact with the modal window before they can return to the parent application.

Using React, this can take the form:

<Modal trigger={<button type="button">Click me</button>}>
  Lorem ipsum in a modal
</Modal>
Enter fullscreen mode Exit fullscreen mode

With a first implementation of the Modal component:

function Modal({ trigger, children }) {
  const [isOpen, setOpen] = useState(false);

  return (
    <>
      {React.cloneElement(trigger, {
        onClick: () => setOpen(true)
      })}
      {isOpen && (
        <div>
          <button
            type="button"
            onClick={() => setOpen(false)}>
            x
          </button>
          <div>{children}</div>
        </div>
      )}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

I removed the class names and the style to focus on the modal logic and semantic. That's a first issue here: the semantic.

The modal is composed with the trigger and the content of the modal window. Except the content isn't qualified as a "modal window" content. Moreover this Modal handles the trigger and the content through different mechanisms:

  • The trigger is a prop, waiting for an element (container + content: here a <button> with a "Click me" text).
  • The lorem ipsum is the content of the component, passed as a rendering node (content only: the Modal wraps the text in a <div>).

And then, there were the subcomponents

A more semantic, consistent version could be:

<Modal>
  <Modal.Trigger>Click me</Modal.Trigger>
  <Modal.Window>
    Lorem ipsum in a modal
  </Modal.Window>
</Modal>
Enter fullscreen mode Exit fullscreen mode

Here the trigger and the window are in the same level, while the lorem ipsum is qualified as the modal window content. In a nutshell, this can be achieved by declaring new components Trigger and Window as properties of Modal. These are React subcomponents. Something like that:

function Modal(/* ... */) {
  /* ... */
}

function Trigger(/* ... */) {
  /* ... */
}

Modal.Trigger = Trigger;

function Window(/* ... */) {
  /* ... */
}

Modal.Window = Window;
Enter fullscreen mode Exit fullscreen mode

Following our previous implementation, Trigger and Window should display the open/close buttons. Modal is a container, and should display its children:

function Modal({ children }) {
  const [isOpen, setOpen] = useState(false);

  return (
    <>
      {children}
    </>
  );
}

function Trigger({ children }) {
  /* ... */

  return (
    <button
      type="button"
      onClick={() => setOpen(true)}>
      {children}
    </button>
  );
}

Modal.Trigger = Trigger;

function Window({ children }) {
  /* ... */

  return isOpen && (
    <div>
      <button
        type="button"
        onClick={() => setOpen(false)}>
        x
      </button>
      {children}
    </div>
  );
}

Modal.Window = Window;
Enter fullscreen mode Exit fullscreen mode

Except isOpen and setOpen are parts of the modal state. So they must be passed to the modal children. A complex prop drilling. Complex because first you will have to "parse" the children to retrieve Trigger and Window... Let's take the easy way out with the Context API:

const ModalContext = createContext();

function Modal({ children }) {
  const [isOpen, setOpen] = useState(false);

  return (
    <ModalContext.Provider value={{ isOpen, setOpen }}>
      {children}
    </ModalContext.Provider>
  );
}

function Trigger({ children }) {
  const { setOpen } = useContext(ModalContext);

  return (
    <button
      type="button"
      onClick={() => setOpen(true)}>
      {children}
    </button>
  );
}

Modal.Trigger = Trigger;

function Window({ children }) {
  const { isOpen, setOpen } = useContext(ModalContext);

  return isOpen && (
    <div>
      <button
        type="button"
        onClick={() => setOpen(false)}>
        x
      </button>
      {children}
    </div>
  );
}

Modal.Window = Window;
Enter fullscreen mode Exit fullscreen mode

What a beauty! Or is it really?

The HTML first approach

It was. Really. Such a beauty this was added to HTML ages ago. An element with an open/close state, triggered by a child, and controlling the display of its content. There are the <details> and <summary> tags. They make our Modal become:

function Modal({ children }) {
  return <details>{children}</details>;
}

function Trigger({ children }) {
  return <summary>{children}</summary>;
}

Modal.Trigger = Trigger;

function Window({ children }) {
  return <div>{children}</div>;
}

Modal.Window = Window;
Enter fullscreen mode Exit fullscreen mode

A complete demo with some style is available here: https://codepen.io/rocambille/pen/poaoKYm.

Sometimes, we want things. And sometimes, we want them so hard we start writing code. Using JS or any other language/tool/framework, because that's what we learned. Using pure CSS when possible.

Sometimes we should do HTML before doing CSS, or JS... or React. Using an HTML first approach ;)

SurveyJS custom survey software

Simplify data collection in your JS app with a fully integrated form management platform. Includes support for custom question types, skip logic, integrated CCS editor, PDF export, real-time analytics & more. Integrates with any backend system, giving you full control over your data and no user limits.

Learn more

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more