How to do a Modal in React: the HTML first approach
Romain Guillemot
Posted on May 2, 2022
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>
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>
)}
</>
);
}
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>
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;
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;
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;
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;
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 ;)
Posted on May 2, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.