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.
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
- The Dialog element (MDN)
- Dialog initial focus, a proposal
- Modal & Nonmodal Dialogs: When (& When Not) to Use Them
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