Build a Dialog Component in React
4 min readAlmost all user interfaces incorporate a dialog at some point. In this post we will look at how we can use React to create an accessible Modal component using the HTML5 dialog element.
If you haven’t heard of the dialog
element yet, it came on the scene in HTML 5.2 and is steadily gaining browser support. However, it is still not a popular solution for most libraries.
I recently watched HTML isn’t done, which discussed how we, as web developers, aren’t using native elements and instead opt for third-party libraries or our own solutions. We usually do this because of specific customizations and thus end up inheriting the burden of shipping and maintaining more code. Not to mention, we also take on the responsibility of making it accessible. Thankfully for us, the dialog element is accessible, customizable, and meets most of our needs out of the box!
First, we start with a simple React component that returns a dialog
element and sets up a ref
so we can interact with our dialog programatically:
1function Modal({ children }) {2 const dialogRef = React.useRef(null)3 return <dialog ref={dialogRef}>{children}</dialog>4}
Next, we need a way to control the open and closed state of the dialog
. We can call either the show
or showModal
methods depending on how we want our dialog to behave. For today, we will only be needing showModal
since this adds a backdrop and traps focus for us:
1function Modal({ children, open }) {2 const dialogRef = React.useRef(null)34 React.useEffect(() => {5 const dialogNode = dialogRef.current6 if (open) {7 dialogNode.showModal()8 } else {9 dialogNode.close()10 }11 }, [open])1213 return <dialog ref={dialogRef}>{children}</dialog>14}
This covers opening and closing our Modal
component using a prop, but there are other ways of closing a dialog
— like pressing the Escape
key. We can listen to the cancel
event to catch this and provide users of our component a way to update the open
prop:
1function Modal({ children, open, onRequestClose }) {2 const dialogRef = React.useRef(null)34 React.useEffect(() => {5 const dialogNode = dialogRef.current6 if (open) {7 dialogNode.showModal()8 } else {9 dialogNode.close()10 }11 }, [open])1213 React.useEffect(() => {14 const dialogNode = dialogRef.current15 const handleCancel = (event) => {16 event.preventDefault()17 onRequestClose()18 }19 dialogNode.addEventListener('cancel', handleCancel)20 return () => {21 dialogNode.removeEventListener('cancel', handleCancel)22 }23 }, [onRequestClose])2425 return <dialog ref={dialogRef}>{children}</dialog>26}
One last thing we should add is returning focus to the element that opened our Modal
when it is closed. This functionality is suggested from WAI-ARIA. However, it is not part of the dialog spec so we need to implement it ourselves:
1function Modal({ children, open, onRequestClose }) {2 // ...3 const lastActiveElement = React.useRef(null)45 React.useEffect(() => {6 const node = ref.current7 if (open) {8 lastActiveElement.current = document.activeElement9 node.showModal()10 } else {11 node.close()12 lastActiveElement.current.focus()13 }14 }, [open])15 // ...16}
Easy enough!
Summary
In this post, we looked at how we can wrap up HTML elements and their imperative APIs in an easy-to-use declarative component. Even if a native HTML solution doesn’t suffice right now, I encourage you to assess building a component with a simplified interface that can eventually be swapped with native functionality as it becomes available. The best part of a component model is we can change the underlying implementation without affecting the top-level API, and as a result, reduce breaking changes. Josh Comeau said this best in his React Rally talk on Explorable Explanations.
To keep this post concise, we didn’t have time to cover styling, clicking outside to close, or other features like loading a polyfill for unsupported browsers. Lucky for you, I put together an example on Codesandbox so you can get an idea of how all of these features work together.
- react
- dialog
- accessibilty
- tutorial