Most modal libraries exist to solve four problems: stacking the overlay above everything else, trapping focus, closing on Escape or an outside click, and wiring up the accessibility attributes. HTML now solves those problems with two built-in mechanisms that are easy to confuse. The popover attribute creates non-modal overlays such as menus, toasts, and tooltips. The <dialog> element, opened with showModal(), creates true modals that block the rest of the page. Choosing between them is the main design decision, and once you have made it, very little JavaScript is left.

Baseline Status: What Is Actually Safe

The Popover API first shipped in Chrome 114, Safari 17, and Firefox 125. It is often described as having reached Baseline in April 2024, when Firefox shipped it, and web.dev published a post saying so. That post was wrong, and web.dev corrected it. On iOS and iPadOS, clicking outside an open popover did not dismiss it, and that behavior, known as light dismiss, is core to the feature. Safari 18.3 fixed it, and the feature became Baseline Newly Available on January 27, 2025. The web.dev correction attributes the miss to a lack of mobile test coverage in the shared test suite.

The practical reading: the core popover attribute works in current versions of all three engines. It is Newly Available, not Widely Available, so if your audience includes older devices that do not update, check your analytics before dropping a fallback.

The <dialog> element is older and more settled. MDN lists it as Widely Available since March 2022. The newer closedby attribute on dialog is a different matter, and we come back to it below.

Choosing Between Popover and Dialog

The distinction is modality, and it has real consequences.

  • A modal dialog (showModal()) puts the dialog in the top layer, renders a ::backdrop, makes everything outside it inert, and closes on Escape. The user must deal with it before continuing.
  • A popover is always non-modal. It also lives in the top layer, so no z-index tuning is needed, but the rest of the page stays interactive. With the default popover="auto" value, it closes on Escape, on an outside click (light dismiss), and when another auto popover opens.

If the content blocks the task (confirm a deletion, collect required input), use a dialog. If it supplements the task (a navigation menu, a help panel, a notification), use a popover. A rule of thumb: if you would be comfortable with the user ignoring it and clicking elsewhere, it is a popover.

Three Patterns, Minimal Code

A menu or disclosure panel

<button popovertarget="site-menu">Menu</button>
<nav id="site-menu" popover aria-label="Site">
  <ul>
    <li><a href="/css/">CSS</a></li>
    <li><a href="/accessibility/">Accessibility</a></li>
  </ul>
</nav>

No script is required. The browser toggles the popover when the button is activated, closes it on Escape or an outside click, and exposes the trigger-to-popover relationship to assistive technology. Style the open state with the :popover-open pseudo-class. By default, a popover is centered in the viewport with UA styles; to place it next to its trigger, pair it with CSS anchor positioning.

A toast notification

Toasts should not steal light dismiss behavior from other UI, so use popover="manual". A manual popover ignores outside clicks and does not close other popovers. It also has no trigger button, so you open it from script:

<div id="toast" popover="manual" role="status">Saved.</div>
const toast = document.getElementById("toast");

function showToast(message) {
  toast.textContent = message;
  toast.showPopover();
  setTimeout(() => toast.hidePopover(), 4000);
}

The role="status" live region is worth keeping: the popover mechanism makes the element visible, but it does not by itself announce changes to screen readers. Placing the live region in the DOM before the message changes is what lets most screen readers pick it up, so keep the element in the markup rather than creating it on demand.

A modal dialog

<button id="open-confirm">Delete draft</button>

<dialog id="confirm">
  <form method="dialog">
    <h2>Delete this draft?</h2>
    <p>This cannot be undone.</p>
    <button value="cancel" autofocus>Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>
const dialog = document.getElementById("confirm");
document.getElementById("open-confirm").addEventListener("click", () => dialog.showModal());
dialog.addEventListener("close", () => {
  if (dialog.returnValue === "delete") deleteDraft();
});

A form with method="dialog" closes the dialog on submit and sets dialog.returnValue to the value of the button that was pressed, so the decision arrives through the close event without any custom state. Putting autofocus on the safe action, here Cancel, gives keyboard users a sensible starting point. MDN recommends autofocus on the close button or primary action for this reason. Style the dimmed layer behind the dialog with dialog::backdrop.

What Still Needs Your Attention

Native does not mean complete. Four gaps show up in real projects.

Menus are not role="menu". A popover gives you visibility, top-layer stacking, and dismissal. It does not add arrow-key navigation or application-menu semantics. For site navigation, a list of links inside a popover is appropriate and needs nothing more. For an application-style menu with arrow-key roving focus, you still need script and ARIA, covered in our ARIA roles guide.

Outside-click closing on dialogs is not yet Baseline. The closedby="any" attribute adds light dismiss to a dialog. It works in Chrome 134 and later and in Firefox 141 and later, but according to Can I Use, Safari does not support it through version 27. Until it does, add a small click handler that closes the dialog when the click lands on the dialog element itself, or accept that modals close only on Escape and through their own buttons.

Opening with the open attribute skips modality. Setting open in markup shows a non-modal dialog. For a modal, call showModal(). MDN also warns against toggling the open attribute repeatedly; use the methods.

Animation takes extra CSS. Top-layer elements toggle between display: none and their visible state, so entrance and exit transitions need @starting-style and transition-behavior: allow-discrete. That is optional polish, not a blocker.

One combination is worth knowing: an element can be both <dialog> and popover. A <dialog popover> opens as a non-modal dialog controlled by popovertarget, which suits a lightweight panel that wants dialog semantics without blocking the page.

Start by auditing what your current modal library actually does. If most of it is stacking, focus, Escape handling, and backdrop styling, replace it with <dialog> for blocking interactions and popover for everything else, then keep small, purpose-written scripts for the two gaps above. The result is less code to ship and behavior that stays consistent as browsers update.