souporserious

Build a Dialog Component in React

4 min read

Almost 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)
3
4 React.useEffect(() => {
5 const dialogNode = dialogRef.current
6 if (open) {
7 dialogNode.showModal()
8 } else {
9 dialogNode.close()
10 }
11 }, [open])
12
13 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)
3
4 React.useEffect(() => {
5 const dialogNode = dialogRef.current
6 if (open) {
7 dialogNode.showModal()
8 } else {
9 dialogNode.close()
10 }
11 }, [open])
12
13 React.useEffect(() => {
14 const dialogNode = dialogRef.current
15 const handleCancel = (event) => {
16 event.preventDefault()
17 onRequestClose()
18 }
19 dialogNode.addEventListener('cancel', handleCancel)
20 return () => {
21 dialogNode.removeEventListener('cancel', handleCancel)
22 }
23 }, [onRequestClose])
24
25 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)
4
5 React.useEffect(() => {
6 const node = ref.current
7 if (open) {
8 lastActiveElement.current = document.activeElement
9 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.

Edit Build a dialog component in React

Updated:
  • react
  • dialog
  • accessibilty
  • tutorial
Next post
Lazy Loading Polyfills