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;
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;
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 */
}
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)
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!
I made it into an npm package for a training project: npmjs.com/package/react-basic-moda...
Great. The npm page looks neat. I'll try it when I have time :)
Thanks for this well detailed yet concise guide!
A few comments though:
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 theisOpened
prop isn't being updated uponEsc
, hence clicking theOpen "dialog" modal
button doesn't change the state and theuseEffect
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 theEsc
key is pressed after shifting the focus from the dialog element) and callonClose
afterEsc
was pressed.Just a minor tedious TypeScript suggestion: replace
const ref: any = useRef(null)
withconst ref = useRef<HTMLDialogElement>(null)
.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!
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
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.