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

rocambille

Romain Guillemot

Posted on May 2, 2022

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 ;)

💖 💪 🙅 🚩
rocambille
Romain Guillemot

Posted on May 2, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related