An extremely detailed overview of how to use the native <dialog>
element in React to build a state-driven modal component. As ever, the answer is significantly more complicated than it would be outside of React land, but just about every step is thought through here.
The one issue I had when following along was the onKeyDown
event listener. Assigning this to the <dialog>
 directly only works if the user uses the Escape key whilst focused on the modal. If they click the background, or anywhere else on the browser, then use Escape the modal will still close, but the state will not be updated correctly. I had to modify the code to use a more traditional event listener instead. Here's my final snippet:
interface ModalProps { title: string isOpen?: boolean onClose?: () => void } export const Modal = ({ title, isOpen = false, onClose, children, ...props }: PropsWithChildren<ModalProps>) => { const modalRef = React.useRef<HTMLDialogElement | null>(null) const [open, setOpen] = React.useState(isOpen) // Function: Closes the modal and syncs state with parent const closeModal = () => { if (onClose) { onClose() } setOpen(false) } // Function: Control modal via props/parent React.useEffect(() => { setOpen(isOpen) }, [isOpen]) // Function: Control the modal with native browser APIs React.useEffect(() => { const modal = modalRef.current if (modal) { if (open) { modal.showModal() } else { modal.close() } } }, [open]) // Function: Listen for escape key and close modal / sync state React.useEffect(() => { const escapeModal = (event: KeyboardEvent) => { if (event.key === "Escape") { if (onClose) { onClose() } setOpen(false) } } document.addEventListener("keydown", (e) => escapeModal(e)) return () => document.removeEventListener("keydown", (e) => escapeModal(e)) }, [onClose]) return ( <dialog ref={modalRef} className="modal" aria-labelledby="modalTitle" {...props} > <header> <h2 id="modalTitle">{title}</h2> <button aria-label="Close"e" onClick={() => closeModal()} > Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â <CloseIcon /> Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â </button> </header> {children} </dialog> ) }
On how to handle native escape key functionality:
However, since we are managing the states of ourModal
component using theuseState
Hook, we need to update it accordingly when the escape key is pressed to ensure the proper functioning of theModal
dialog.