Built-in layered UI: choosing between <dialog> and popover
Popups and overlays are a staple of web UI, handling everything from quick alerts to cookie consent flows. Historically, these patterns required custom JavaScript or a third-party library, bringing with them a long checklist of edge cases and accessibility concerns. Modern browsers now ship two Baseline-supported alternatives: the <dialog> element and the popover attribute.
To illustrate how these features work in practice, consider a common scenario: a user clicks a "favorite" button on an image in a gallery but isn't logged in. The app needs to interrupt the flow with a sign-up prompt. This is a perfect job for a modal <dialog>.
Building a modal dialog
A <dialog> element can create both modal and non-modal dialogs. The key distinction is that a modal dialog makes all content underneath it inert and inaccessible until it's closed. The modal is placed directly in the page markup, right after the gallery content itself.
<dialog aria-labelledby="dialog-title" aria-describedby="description">
<h2 id="dialog-title">Yay! You found a favorite!</h2>
<p id="description">Sign up with your email address to start adding to your favorites list.</p>
</dialog>
Opening with showModal()
The <dialog> is hidden by default. Calling its showModal() method does far more than just make it visible:
const openDialogBtns = document.querySelectorAll(`[data-button*="show-dialog"]`);
const dialog = document.querySelector('dialog');
openDialogBtns.forEach(openDialogBtn => {
openDialogBtn.addEventListener('click', () => {
dialog.showModal();
});
});
- It promotes the
<dialog>to the browser's top layer, guaranteeing it renders above all other content regardless ofz-index. - It attaches a
::backdroppseudo-element directly beneath the dialog, which you can style to dim or blur the page behind it. - It implicitly adds the
aria-modal="true"attribute for assistive technologies. - It automatically marks all other page content as inert.
Recreating these behaviors manually is a significant undertaking. The WAI ARIA Authoring Practices Guide also recommends that a modal should move focus inside the dialog, trap the Tab key within it until it's closed, obscure the background content, and return focus to the opening element upon dismissal. The native showModal() method handles this foundational behavior, including focus management, without bespoke code.
Closing the dialog
Users can dismiss a modal <dialog> with the Esc key. For an explicit close action, you can call the HTMLDialogElement.close() method from a button's click handler.
// Code omitted...
const dialog = document.querySelector('dialog');
const closeDialogBtn = document.getElementById('close');
// Code omitted...
closeDialogBtn.addEventListener('click', () => {
dialog.close();
});
A declarative approach is also available. Adding a <form> with method="dialog" will make any submit event from within that form close the dialog. This pattern will feel familiar if you've used component libraries that offer a DialogClose component.
<dialog aria-labelledby="dialog-title" aria-describedby="description">
<form method="dialog">
<button type="submit">Close</button>
</form>
</dialog>
Popovers for lighter context
For less intrusive layer UI, the popover attribute can be applied to any element. Popovers differ from modal dialogs in several key ways:
- Popovers do not make the rest of the page inert.
- Default popovers (
popover="auto") support light dismissal (clicking outside), while a modal dialog only does if it has theclosedBy="any"attribute. - Popovers have no inherent semantics; a
<dialog>has an implicit ARIA role of"dialog". - Popovers are styled with the
:popover-openpseudo-class, whereas a dialog uses theopenattribute.
In the sign-up example, a popover could be used inside the dialog to offer a brief, non-blocking explanation—for instance, clarifying why an email address is needed.
<form method="dialog">
<label for="email">Enter your email</label>
<button id="popover-trigger" type="button" popovertarget="privacy-popover">
<span class="visually-hidden">How we handle your email</span>
</button>
<p id="privacy-popover" popover>As with all of your information, we promise not to sell your email address</p>
</form>
This works because both patterns utilize the top layer. When one top-layer element opens another, the newest element is placed highest in the layer stack. So the popover appears on top of the dialog automatically, without any manual z-index management. To wire up the toggle, the button acts as a control with its popovertarget attribute set to the id of the target popover element.
Managing focus and input
A modal dialog traps focus within itself and will move focus to the first focusable element when opened. If a different element is a better starting point, use the autofocus attribute. In the demo, the email input field has this attribute, letting users begin typing as soon as the dialog appears.
Styling the backdrop
The ::backdrop pseudo-element is your tool for visually isolating a modal dialog. The demo applies a faint linear-gradient and a blur effect using the backdrop-filter property to soften the content behind the dialog.
To animate the backdrop's gradient colors, the custom properties controlling the color stops must first be registered with the @property rule. This registration tells the browser the expected data type, which is a prerequisite for smoothly transitioning or animating the values. Once registered, you can define the changing color stops with @keyframes or a transition.
@property --backdrop-gradient-start {
syntax: "<color>";
initial-value: oklch(33.894% 0.08072 246.33);
inherits: true;
}
@property --backdrop-gradient-end {
syntax: "<color>";
initial-value: oklch(45.859% 0.00345 174.48 / 0.3);
inherits: true;
}
/* ... */
dialog[open]::backdrop {
opacity: 1;
background: linear-gradient(140deg,
var(--backdrop-gradient-start),
var(--backdrop-gradient-end));
backdrop-filter: blur(2px);
animation: show-gradient var(--transition-timing-slower) forwards;
}
@keyframes show-gradient {
from {
--backdrop-gradient-start: oklch(45.859% 0.00345 174.48 / 0.3);
--backdrop-gradient-end: oklch(33.894% 0.08072 246.33 / 0.3);
}
to {
--backdrop-gradient-start: oklch(33.894% 0.08072 246.33);
--backdrop-gradient-end: oklch(45.859% 0.00345 174.48 / 0.3);
}
}
Animating open and close
Both the dialog element and popover content are hidden with display: none by default. Since CSS transitions can't smoothly interpolate to or from this value in the usual way, their appearance and dismissal are instantaneous. The combination of the @starting-style rule and the transition-behavior property allows you to transition between these states.
To animate a dialog's entrance, you need to define its appearance for three distinct states: open, transitioning-in, and closed. The open state defines the final appearance.
dialog[open] {
--opacity: 1;
--translate: 0 0;
}
The transitioning-in state is defined inside a @starting-style block. This tells the browser what styles to apply as the element first renders and flips from display: none to display: block. Without this block, the transition wouldn't have a starting point to animate from.
/* Open state */
dialog[open] {
--opacity: 1;
--translate: 0 0;
/* Transitioning state */
@starting-style {
--opacity: 0;
--translate: 100vw -10rem;
}
}
Finally, you need explicit transition settings for the element's exit back to its default state.
/* Closed state */
dialog {
opacity: var(--opacity, 0);
translate: var(--translate, 100vw -10rem);
transition:
opacity 1s ease-in,
translate 0.6s ease-in-out
overlay 0.6s ease-in-out,
display 0.6s linear;
transition-behavior: allow-discrete;
}
The transition must include the overlay property, along with opacity, translate, and display, for the animation to function. You'll also need to set transition-behavior: allow-discrete for overlay and display, as these properties normally animate discretely (switching abruptly) rather than continuously.
Platform features over custom code
The <dialog> element and the popover attribute handle the heavy lifting of creating accessible layered UI. Relying on these built-in, Baseline features removes the burden of implementing complex focus management, top-layer rendering, and dismissal logic yourself. This frees you to focus your engineering effort on enhancing the user experience rather than debugging the foundational mechanics of an overlay.



