React modal using <dialog> element | Log Rocket

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 our Modal component using the useState Hook, we need to update it accordingly when the escape key is pressed to ensure the proper functioning of the Modal dialog.

Explore Other Notes

  • <!DOCTYPE html> <html> <head> <title></title> </head> <body> <p>An extremely detailed overview of how to use the native &lt;dialog&gt; element in React to build a state-driven modal component. As ever, the answer is significantly more complicated than it would be …</p> </body> </html>
  • Murray Adcock.
Journal permalink