Why you should use the Native Dialog Element

We're all familiar with dialogs. From simple prompts for confirming an action to content-heavy windows — dialogs are an integral part of modern web user interfaces.

Unfortunately, we didn't have a native dialog element for a long time, leading to different custom implementations with many accessibility issues. This changed when HTML 5.2 introduced the <dialog> element. With Safari finally adding support in version 15.4, all modern browsers now support the element.

Two white speech bubbles on a pink background. Photo: © Miguel Á. Padriñán / pexels.com

I'll show you how easy it is to create an accessible, modal dialog using the native element. Some minor accessibility issues remain with some browsers and screen readers, which I'll cover at the end of the post.

What does the <dialog> element do?

The dialog element creates a popup box on your website that draws the user's attention. The HTML specification states:

The dialog element represents a part of an application that a user interacts with to perform a task, for example a dialog box, inspector, or window.

A typical use case would be a modal dialog that obscures the rest of the page and asks the user for some input. I've created a demo using the React framework (source code):

Basic Setup and Interaction with the Dialog

The basic JSX code of my modal dialog demo looks like this:

<dialog aria-labelledby='dialog-personal-info-heading' ref={formDialogRef} onClick={onFormDialogContainerClick} > <h3 id="dialog-personal-info-heading"> Personal Information </h3> <p>...</p> <form method="dialog" onClick={event => event.stopPropagation()} > // form elements </form> </dialog>

By default, the browser won't display the dialog until you pass in an open property to make it visible. It is recommended to use the .show() or .showModal() methods to render dialogs, rather than the open attribute.

In my case, I want to show a modal dialog that obscures the rest of the page. In my functional React component, I retrieve a reference to the HTML element with the useRef hook and open the dialog on a button click:

const formDialogRef = useRef<HTMLDialogElement>(null); const onOpenFormDialogClick = () => { formDialogRef.current?.showModal(); }

To close the dialog, use the .close() method. As my demo shows, a <form> element can also close the dialog using the attribute method="dialog". When the form is submitted, the dialog closes with its returnValue property set to the value of the button that was used to submit the form:

<form method="dialog"> <div className={styles.formField}> <label htmlFor="favMovie">Favorite movie:</label> <input id="favMovie" type="text" /> </div> <button type="submit" value={DIALOG_CONFIRM_VALUE} > Confirm </button> </form>

You can add an event listener for the dialog's close event and trigger an action depending on the value of the returnValue property.

Styling the Dialog and its Backdrop

The modal dialog is rendered in the center of the page on top of the other content. Browsers apply a default style to the dialog element, usually a thick black border. You can easily customize the dialog's appearance with CSS. For example, add a drop shadow and rounded corners like this:

dialog { border: 0.125rem solid var(--border-color); border-radius: 4px; box-shadow: 0 11px 15px -7px #0003, 0 24px 38px 3px #00000024, 0 9px 46px 8px #0000001f; font-size: 1rem; max-width: min(18rem, 90vw); }

A really awesome feature of the dialog element is the ::backdrop CSS pseudo-element. It allows you to style behind a modal dialog to, e.g., dim and blur the unreachable content of the page:

dialog::backdrop { background: rgba(36, 32, 20, 0.5); backdrop-filter: blur(0.25rem); }

Keyboard and Mouse Control

When the modal dialog is opened, the browser moves focus to the first interactive element inside of the dialog. This works well for many use cases, like my form dialog. But sometimes you would prefer to set the focus on the whole dialog or a text element at the top. Learn more about the issue in this proposal for initial focus placement.

While the modal dialog is active, the content obscured by the dialog is inaccessible to all users. This means that keyboard users can't leave the dialog with the TAB key, and a screen reader's virtual cursor (arrow keys or swiping) is not allowed to leave the modal dialog as long as it remains open.

Users can close the modal dialog with the ESC key. On close, focus returns to the control that initially activated the dialog. This allows keyboard and screen reader users to continue browsing from where they left off.

Unfortunately, the dialog element doesn't close automatically when the user clicks outside of it. If we want to implement this behavior, we can get the coordinates of the click and compare them to the dialog's rectangle (thanks for the idea, Amit Merchant):

const isClickOutsideOfDialog = (dialogEl: HTMLDialogElement, event: React.MouseEvent): boolean => { const rect = dialogEl.getBoundingClientRect(); return (event.clientY < rect.top || event.clientY > rect.bottom || event.clientX < rect.left || event.clientX > rect.right); } const onFormDialogContainerClick = (event: React.MouseEvent) => { const formDialogEl = formDialogRef.current; if (formDialogEl && isClickOutsideOfDialog(formDialogEl, event) ){ formDialogEl.close(DIALOG_CANCEL_VALUE); } }

This works fine, except for some use cases. For example, opening a native select inside the dialog would count as a click outside of the dialog and close it. Therefore, I apply a click event listener to the form element and use the .stopPropagation() method.

Accessibility Issues with some Screen Readers

The native dialog element works well with most screen readers and browsers. But still, some issues remain as the accessibility audits of my demo on different platforms have shown:

  • Windows 10, Google Chrome 103.0.5060.114, NVDA 2022.1: When the dialog is opened and receives focus, the screen reader announces the dialog role, the heading (thanks to aria-labelledby), the first paragraph and the focused select element. Focus order and the screen reader's virtual cursor are limited to the dialog's content.
  • Windows 10, Firefox 102.0.1, NVDA 2022.1: Identical to Google Chrome, except that the button that opened the dialog is part of the focus order. Probably a Firefox bug that will be fixed in the future.
  • Samsung Galaxy S20, Android 12, Google Chrome 103.0.5060.71, TalkBack: The screen reader only announces the focused select element. The virtual cursor (e.g. swipe right) is limited to the dialog's content.
  • Samsung Galaxy S20, Android 12, Firefox 102.2.1, TalkBack: The screen reader only announces the focused select element. The virtual cursor is not limited to the dialog's content. This is probably due to Firefox still not supporting the aria-modal property.
  • iPhone 8, iOS 15.5, Safari, VoiceOver: The screen reader only announces the focused select element. The virtual cursor can be moved outside the dialog to the elements in the header, but not to the elements in the main content section.

Conclusion

The <dialog> element is easy to use and accessible by default, apart from some minor issues. Right now, a robust custom dialog like a11y-dialog might still be the better option for some use cases. But I'm very optimistic about the native element's future.

Useful Resources

Update on 03/07/2023

The HTML specification has received an important update regarding initial focus management. It will be possible to make the dialog element itself get focus if it has the autofocus attribute set.

Sure, it'll take some time until browser vendors implement these changes. But there's no reason to wait any longer! I agree with Scott O'Hara: “Instead of waiting for perfect, I personally think it’s time to move away from using custom dialogs, and to use the dialog element instead.”

Posted on