A dropdown, a tooltip, a confirmation window, and a newsletter prompt may all look like rectangles floating above a page. They do not have the same job. Choosing between the Popover API and <dialog> starts with what the user should be allowed to do while that rectangle is open.
The Popover API creates non-modal overlays. A modal dialog temporarily makes the rest of the document unavailable. That distinction affects focus, dismissal, semantics, and how much behavior the browser can handle for you.
Start with behavior, not appearance
Popover
The page remains interactive. Use it for menus, pickers, teaching UI, and other optional floating content.
Modal dialog
The user must address the dialog before returning to the page. Use it for confirmations and short tasks that require a decision.
A menu should not block the page, so a popover is usually the better match. A destructive confirmation should interrupt the current task, so a modal dialog is usually correct. CSS cannot turn one behavior into the other. A dark backdrop behind a <div> does not make that element modal.
Use a popover when the page should stay available
The smallest useful popover needs a button, a target element, and a shared identifier. The browser toggles it, places it in the top layer, closes it when the user presses Escape, and provides light dismiss when the user clicks elsewhere.
For one declarative trigger pattern across both elements, command and commandfor can control popovers and dialogs without a click handler.
<button popovertarget="account-menu">
Account
</button>
<nav id="account-menu" popover aria-label="Account">
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
</nav>
With no value, popover means popover="auto". Opening another auto popover normally closes the first one. That behavior suits menus and pickers where several independent panels should not pile up.
Use popover="manual" when the panel must stay open until your code closes it. A persistent toast is one example. Manual popovers do not gain automatic light dismiss, so they usually need a clear close control and a small amount of JavaScript.
Popover does not mean menu. Keep the HTML inside the panel appropriate for its content. A list of site links can stay a normal navigation list. Do not add role="menu" unless you also intend to implement the keyboard model that ARIA menus require.
Use a modal dialog when the task must pause
Calling showModal() opens a dialog in the top layer and makes the rest of the document inert. The browser moves focus into the dialog and returns focus to the invoking control when the dialog closes.
<button id="delete-account">Delete account</button>
<dialog id="delete-confirmation">
<h2>Delete this account?</h2>
<p>This action cannot be undone.</p>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="confirm">Delete</button>
</form>
</dialog>
<script>
const trigger = document.querySelector("#delete-account");
const dialog = document.querySelector("#delete-confirmation");
trigger.addEventListener("click", () => dialog.showModal());
</script>
The opening action still needs JavaScript because it changes application state. Closing buttons inside <form method="dialog"> do not need click handlers. Their values are available through dialog.returnValue, which lets the script respond after the dialog closes.
Do not open a modal dialog by adding the open attribute yourself. That creates a non-modal dialog and skips the behavior supplied by showModal().
A practical decision rule
- Use a popover if the user can reasonably keep interacting with the page.
- Use
<dialog>withshowModal()if the rest of the page must wait. - Use ordinary in-flow HTML when the content does not need to float above anything.
There is a valid hybrid: <dialog popover>. It gives a non-modal popover dialog semantics and declarative controls. It can suit a substantial inspector or subwindow that should be announced as a dialog without blocking the page. Most menus and confirmations do not need the hybrid.
Focus and accessibility still need a deliberate pass
Native behavior removes a lot of fragile code, but it cannot decide where the most useful first focus target is. Give a modal dialog a clear heading. Keep the close action easy to find. Test Escape, Tab, Shift+Tab, screen reader announcements, and focus restoration.
A popover trigger receives an implicit relationship with its target. That is helpful, but the content still needs useful semantics. If the panel is navigation, use <nav>. If it is a group of actions, use real buttons. The Popover API handles visibility, not the meaning of the controls inside it.
[popover]::backdrop {
background: rgb(23 23 23 / 0.12);
}
dialog::backdrop {
background: rgb(23 23 23 / 0.58);
}
The visual treatment can reinforce the behavioral difference. A light popover backdrop should not suggest that the page is disabled. A modal dialog can use a stronger backdrop because the page really is unavailable.
Replace custom overlays safely
Before replacing a custom overlay, write down what it currently does. Check whether clicking outside closes it, where focus moves, whether Escape is supported, what happens on navigation, and whether analytics depend on existing events. Then map those behaviors to the platform feature.
The good migration is rarely "delete every line of JavaScript." It is usually "let HTML own visibility and focus, then keep the small piece of application logic that remains." That is still a useful result. The browser code has been tested in more situations than a local overlay helper ever will be.
If geometry is the remaining custom layer, the CSS anchor positioning guide explains how to attach a popover to its trigger and provide collision fallbacks.