In frontend development, building or using pre-built UI components is a common task. However, these components often come with limitations. They are typically tied to specific frameworks and require complex, non-standardized logic. For a long time, basic UI components like dialog windows relied on custom implementations or, in more simple cases, built-in JavaScript methods such as alert()
, prompt()
, and confirm()
.
The great news is that you can now implement this component using the native <dialog>
HTML element, which is part of the HTML5 standard and fully supported by all modern browsers.
The <dialog>
HTML tag, introduced in May 2013 as part of a W3C working draft, was launched together with interactive elements like <details>
and <summary>
to address common UI challenges. Released in 2014, <dialog>
was initially supported only in Google Chrome and Opera. Full support for <dialog>
in Firefox and Safari came only in March 2022, delaying its adoption in production projects. However, with over two years of support from major browsers, the <dialog>
element is now stable enough to replace custom <div class="modal" tabindex="-1" role="dialog" aria-modal="true">
implementations.
Let's explore the features of <dialog>
in more detail.
Core aspects of usage
The HTML <dialog>
tag creates a hidden by default dialog box that can work as either a popup or a modal window.
Popups are frequently used to display simple notifications, such as cookie messages, disappearing toast alerts, tooltips, or right-click context menu elements.
Modal windows help users focus on specific tasks, such as notifications and warnings requiring user confirmation, complex interactive forms, and lightboxes for images or videos.
Popups don't prevent interaction with the page, while modal windows overlay the document, dim the background, and block other actions. This behavior works without additional styles or scripts; the only difference is the method used to open the dialog.
Dialog window opening methods
— popup:
<dialog id="pop-up">Hello, I'm a popup!</dialog>
const popUpElement = document.getElementById("pop-up");
popUpElement.show();
— modal window:
<dialog id="modal">Hi, I'm a modal window!</dialog>
const modalElement = document.getElementById("modal");
modalElement.showModal();
In both cases, opening the <dialog>
tag sets its open
attribute to true
. Setting it directly will open the dialog as a popup, not a modal. To render modal windows, you must use the appropriate method. No JavaScript is needed to create an initially opened popup.
<dialog open>Hi, I'm a popup!</dialog>
Try it out:
- Opening a popup using the
.show()
method: https://codepen.io/alexgriss/pen/zYeMKJE - Opening a modal window using the
.showModal()
method: https://codepen.io/alexgriss/pen/jOdQMeq - Directly changing the
open
attribute: https://codepen.io/alexgriss/pen/wvNQzRB
Dialog window closing approaches
Dialog windows close the same way, no matter how they were opened. Here are a few ways to close a popup or modal window:
— with the .close()
method:
const dialogElement = document.getElementById("dialog");
dialogElement.close();
— by triggering the submit
event in a form with the method="dialog"
attribute:
<dialog>
<h2>Close me!</h2>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
— by pressing the Esc key:
Closing with Esc works for modal windows only. It first triggers the cancel
event, then close
, making it easy to warn users about unsaved changes in forms.
Try it out:
- Closing a dialog box with the
close
method: https://codepen.io/alexgriss/pen/GRzwjaV - Closing a dialog box through the
submit
form: https://codepen.io/alexgriss/pen/jOdQVNV - Closing a modal window with the Esc key: https://codepen.io/alexgriss/pen/KKJrNKW
- Preventing a modal window from closing with Esc: https://codepen.io/alexgriss/pen/mdvQObN
Return value on close
When closing a dialog with a form using the method="dialog"
attribute, you can capture the value of the submit button. This is useful if you want to trigger different actions based on the button clicked. The value is stored in the returnValue
property.
Try it out: https://codepen.io/alexgriss/pen/ZEwmBKx
A closer look at how it works
Let's dive deeper into the mechanics of the dialog window and the details of its browser implementation.
The mechanics of a popup
Opening a <dialog>
as a popup with .show()
or the open
attribute automatically positions it with position: absolute
in the DOM. Basic CSS styles, like margins and borders, are applied to the element, and the first focusable item inside the window will automatically get focus through the autofocus
attribute. The rest of the page remains interactive.
The mechanics of a modal window
A modal window is designed and works in a more complex way than a popup.
Document overlaying
When opening a modal window with .showModal()
, the <dialog>
element is rendered in a special HTML layer that covers the entire visible area of the page. This layer is called the top layer
and is controlled by the browser. In some browsers like Google Chrome, each modal is rendered in a separate DOM node within this layer, visible in the element inspector.
The concept of layers refers to the stacking context, which defines how elements are positioned along the Z-axis relative to the user. Setting a z-index
value in CSS creates a stacking context for an element, where the position of its children is calculated within that context. Modal windows are always at the top of this hierarchy, so no z-index
is needed.
Learn more about the stacking context on MDN.
To learn more about the elements rendered in the top layer, visit MDN.
Document blocking
When a modal element is rendered in the top layer, a ::backdrop
pseudo-element is created with the same size as the visible document area. This backdrop prevents interaction with the rest of the page, even if pointer-events: none
CSS rule is set.
The inert
attribute is automatically set for all elements except the modal window, blocking user actions. It disables click and focus events and makes the elements inaccessible to screen readers and other assistive technologies.
Learn more about the inert
attribute on MDN.
Focus behavior
When the modal opens, the first focusable element inside it automatically gets focus. To change the initially focused element, you can use the autofocus
or tabindex
attributes. Setting tabindex
for the dialog element itself isn't possible, as it's the only element on the page where inert
logic doesn't apply.
After the dialog is closed, the focus returns to the element that opened it.
Solving UX problems with modal windows
Unfortunately, the native implementation of the <dialog>
element doesn't cover all aspects of interacting with modal windows. Next, I'd like to go over the main UX issues that may come up when using modal windows and how to solve them.
Scroll blocking
Even though the native HTML5 modal window creates a ::backdrop
pseudo-element that blocks interaction with the content underneath it, the page scroll is still active. This can be distracting for users, so it's recommended to cut off the content of the body
when the modal opens:
body {
overflow: hidden;
}
Such a CSS rule will have to be dynamically added and removed each time the modal window is opened and closed. This can be achieved by manipulating a class containing this CSS rule:
// When the modal is opened
document.body.classList.add("scroll-lock");
// When the modal is closed
document.body.classList.remove("scroll-lock");
You can also use the :has
selector if its support status meets the project's requirements.
body:has(dialog[open]) {
overflow: hidden;
}
Try it out: https://codepen.io/alexgriss/pen/XWOyVKj
Closing the dialog by clicking outside the window
This is a standard UX scenario for a modal window and it can be implemented in several ways. Here are two ways to solve this problem:
A method based on the behavior of the ::backdrop
pseudo-element
Clicking on the ::backdrop
pseudo-element is considered a click on the dialog element itself. Therefore, if you wrap the entire content of the modal window in an additional <div>
and then cover the dialog element itself, you can determine where the click was directed — on the backdrop or on the modal window content.
Don't forget to reset the browser's default padding and border styles for the <dialog>
element to prevent the modal window from closing on accidental clicks:
dialog {
padding: 0;
border: none;
}
Now, we apply the common styles for the modal window's borders and margins only to the inner wrapper.
We need to write a function that will close the modal window only when clicking on the backdrop, not on the inner wrapper element:
const handleModalClick = ({ currentTarget, target }) => {
const isClickedOnBackdrop = target === currentTarget;
if (isClickedOnBackdrop) {
currentTarget.close();
}
};
modalElement.addEventListener("click", handleModalClick);
Try it out: https://codepen.io/alexgriss/pen/mdvQXpJ
A method based on determining the size of the dialog window
This method is different from the first one, which needed an extra wrapper for the modal content. Here, you don't need any extra wrapping. All that's required is to check if the cursor's position goes outside the element's area when clicked:
const handleModalClick = (event) => {
const modalRect = modalElement.getBoundingClientRect();
if (
event.clientX < modalRect.left ||
event.clientX > modalRect.right ||
event.clientY < modalRect.top ||
event.clientY > modalRect.bottom
) {
modalElement.close();
}
};
modalElement.addEventListener("click", handleModalClick);
Try it out: https://codepen.io/alexgriss/pen/NWoePVP
Styling the dialog window
The <dialog>
element is more flexible in terms of styling than many native HTML elements. Here are some examples to style dialog windows:
Styling the backdrop using the ::backdrop
selector: https://codepen.io/alexgriss/pen/ExrOQEO
Animated dialog window opening and closing: https://codepen.io/alexgriss/pen/QWYJQJO
Modal window as a sidebar: https://codepen.io/alexgriss/pen/GRzwxgr
Accessibility
For a long time, the <dialog>
element had some accessibility problems, but now it works well with major assistive technologies like screen readers (VoiceOver, TalkBack, NVDA).
When a <dialog>
is opened, the focus is moved to the dialog by the screen reader. For modal windows, the focus remains within the dialog until it is closed.
By default, the <dialog>
element is recognized by assistive technologies as having the ARIA attribute role="dialog"
. A modal dialog will be recognized as having the ARIA attribute aria-modal="true"
.
Here are some ways to improve the accessibility of the <dialog>
element:
aria-labelledby
Always include a title inside dialog windows and specify the aria-labelledby
attribute for the <dialog>
element, with the value set to the id
of the title.
<dialog aria-labelledby="dialog-header">
<h1 id="dialog-header">Dialog Header</h1>
</dialog>
In this case, screen readers will read the content of this title when the dialog window is opened.
aria-describedby
The aria-describedby
attribute should be used to associate with the dialog content. Without this attribute, some screen readers may not be able to read the <dialog>
content. Headings and any interactive elements for controlling the dialog's state should be placed outside the content element.
<dialog aria-labelledby="dialog-header" aria-describedby="dialog-content">
<h1 id="dialog-header">Dialog Header</h1>
<div id="dialog-content">Dialog Content</div>
<button id="close-btn">Close dialog</button>
</dialog>
aria-label
Always add a button to close dialog windows, especially inside modal windows. For better accessibility, use the <button>
element. For buttons that don't contain obvious text for the user, specify this text in the aria-label
attribute:
<dialog aria-labelledby="dialog-header">
<button id="close-btn" aria-label="Close dialog">x</button>
<h1 id="dialog-header">Dialog Header</h1>
</dialog>
Browser support
The native HTML5 <dialog>
element is a convenient and powerful tool for solving common UI problems. Unfortunately, its support in more exotic or outdated browsers may still be missing. If it's not supported in your browser, you can use the polyfill created by the Google Chrome team.
You can connect the polyfill scripts and styles locally, use a CDN, or install it as an npm dependency: npm install dialog-polyfill
.
If you're using a CDN, don't forget to include the stylesheet:
<link
rel="stylesheet"
href="https://unpkg.com/dialog-polyfill/dialog-polyfill.css"
/>
If you need to style the ::backdrop
pseudo-element, ensure you also apply those styles to the corresponding .backdrop
element to ensure compatibility with older browsers:
dialog::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
dialog + .backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
It's recommended to connect the polyfill via a dynamic import and only for browsers that don't support the <dialog>
element:
const isBrowserNotSupportingDialog = window.HTMLDialogElement === undefined;
if (isBrowserNotSupportingDialog) {
const dialogElement = document.getElementById("modal");
const { default: polyfill } = await import("dialog-polyfill");
polyfill.registerDialog(dialogElement);
}
Conclusion
The native HTML5 <dialog>
element is a relatively simple yet powerful tool for implementing modal windows and popups. It's well supported by modern browsers and can be successfully used in projects based on both vanilla JS and any frontend framework.
In this article, we discussed the following topics:
- Problems the
<dialog>
element solves; - Interaction with the
<dialog>
element's API; - How dialog windows work at the browser level;
- Common issues with modals and their solutions;
- Improving accessibility of the
<dialog>
element for assistive technologies like screen readers; - Expanding browser support for the
<dialog>
element.
Finally, I invite you to check out the modal window component implementation in pure JS, which takes into account the main aspects discussed in the article: https://codepen.io/alexgriss/pen/abXPOPP
That's all I wanted to share about working with the <dialog>
HTML element. I hope this article inspires you to experiment!
Top comments (0)