Popover API vs dialog: How to Choose an Overlay

Use <dialog> with showModal() when the rest of the page has to stop responding: a confirmation, a destructive action, a checkout step the user must answer. Use the Popover API for menus, tooltips, toasts and disclosure panels that sit above the page without blocking it. Popovers are never modal. A dialog can be either.

Both put their element in the top layer, so both escape z-index and overflow: hidden on ancestors. That is where the similarity stops. The difference that matters is what happens to everything else on the page.

What does showModal() do that a popover cannot?

showModal() makes the rest of the document inert. Clicks, taps and tab stops outside the dialog stop working, the dialog picks up an implicit aria-modal="true", and focus moves to the element carrying autofocus or to the first focusable element inside. Close it and focus returns to whatever opened it. Escape fires a cancelable cancel event, then close.

It also has a submission model no popover has. A <form method="dialog"> closes the dialog without a network request and sets returnValue to the value of the button that submitted it.

<dialog id="confirm-delete">
  <form method="dialog">
    <p>Delete this invoice? This cannot be undone.</p>
    <button value="cancel" autofocus>Keep it</button>
    <button value="delete">Delete</button>
  </form>
</dialog>

<script>
  const dialog = document.getElementById('confirm-delete');
  dialog.addEventListener('close', () => {
    if (dialog.returnValue === 'delete') deleteInvoice();
  });
</script>

<dialog> has been Baseline widely available since March 2022, so there is no support argument left against it.

What does the Popover API give you for free?

Add popover to any element and a button gets popovertarget pointing at its id. No JavaScript, no focus library, no outside-click listener.

The attribute takes three values. popover="auto" light dismisses on Escape or a click outside, and opening one auto popover closes the others unless they are nested. popover="manual" closes only when you tell it to. popover="hint" sits in its own stack: showing a hint closes other hints but leaves auto popovers alone, which is what you want for a tooltip appearing over an open menu.

Wiring a button with popovertarget also gives you implicit aria-expanded on the invoker, an implicit aria-details relationship, and a tab order that treats the popover as the next stop after the button. popovertargetaction takes show, hide or toggle. From script the methods are showPopover(), hidePopover() and togglePopover(), and both beforetoggle and toggle fire as a ToggleEvent, so you can cancel an open in beforetoggle.

Focus is the thing people trip over. Opening an auto popover does not move focus into it. That is correct for a tooltip and wrong for a menu, so a menu still needs arrow key handling of its own. Our notes on accessible UI and WCAG compliance go into what auditors actually check here.

The Popover API reached Baseline newly available on 27 January 2025.

Popover API vs dialog: which one for which component?

A dropdown menu, a filter panel, a share sheet, a combobox listbox: popover="auto". The page behind stays usable, one click anywhere else closes it, and the browser handles the stacking when a submenu opens inside a menu.

A tooltip: popover="hint" where it is supported, falling back to manual where it is not, since an unsupporting browser parses an unknown value as manual. Position it with CSS anchor positioning rather than a positioning library; we covered that swap in our post on replacing Floating UI.

A toast or an undo snackbar: popover="manual". An auto popover would be closed by the next menu the user opens, which is not how a toast should behave.

A confirmation, a destructive action, a payment step, a form the user must finish or abandon: <dialog> with showModal(). If the answer changes what happens next, the page behind should be inert.

The one combination worth avoiding is the non-modal dialog opened with show(). It has no backdrop, no inert, no Escape handling by default, and no light dismiss. Almost everything people reach for it to build is a popover.

Can you nest a popover inside a dialog?

Yes, and the top layer sorts it out. The last element added to the top layer sits highest, so a popover opened from inside an open modal dialog renders above it with no z-index involved. <dialog popover> is also valid if you want dialog semantics with popover invocation.

One behaviour to know: a successful showModal() call dismisses open auto popovers. Opening a modal over a menu closes the menu, which is usually right, but it means you cannot keep an auto popover visible next to a modal.

How do you open and close them without JavaScript?

The Invoker Commands API replaces the onclick handler. A <button> takes commandfor (the target id) and command (the action). For dialogs the built-in commands are show-modal, close and request-close. For popovers they are show-popover, hide-popover and toggle-popover.

<button command="show-modal" commandfor="settings">Settings</button>

<dialog id="settings" closedby="any">
  <div class="dialog-body">
    <h2>Settings</h2>
    <button command="request-close" commandfor="settings">Done</button>
  </div>
</dialog>

request-close is the one to use on a close button. It fires cancel first, so a form with unsaved changes can call preventDefault() and keep the dialog open, exactly as the Escape key does. The scripted equivalent is requestClose(), which has been Baseline newly available since May 2025.

Custom commands are allowed if they start with two dashes, and they arrive as a CommandEvent on the target with the name in event.command:

document.getElementById('preview').addEventListener('command', (event) => {
  if (event.command === '--rotate-left') rotate(-90);
});

Invoker commands became Baseline newly available in December 2025, landing in Chrome 135, Firefox 144 and Safari 26.2. Buttons using them still need a scripted fallback if you support older Safari, because an unrecognised command attribute does nothing at all.

Can a dialog be dismissed by clicking the backdrop?

That is what closedby is for. closedby="any" gives a dialog the same light dismiss a popover has, closedby="closerequest" allows Escape but not an outside click, and closedby="none" leaves it entirely to your own close button. Without the attribute, a dialog opened with showModal() behaves as closerequest and one opened any other way behaves as none.

The catch, as of September 2026, is Safari. closedby shipped in Chrome 134 in March 2025 and Firefox 141 in July 2025, and WebKit has not implemented it, so it is not Baseline. Escape still works everywhere; only the outside click needs propping up:

if (!('closedBy' in HTMLDialogElement.prototype)) {
  dialog.addEventListener('click', (event) => {
    if (event.target === dialog) dialog.requestClose();
  });
}

That event.target === dialog test is doing real work. A click anywhere in the dialog’s own padding also reports the dialog as the target, so keep the padding on an inner wrapper and the dialog element itself free of it, or the first miss-click closes the thing.

Why does the exit animation flicker?

Both elements toggle display, and both leave the top layer the moment they close, so a plain transition on opacity plays for the entry and gets cut off on the exit. Three pieces fix it: @starting-style for the entry values, allow-discrete on display, and allow-discrete on overlay to hold the element in the top layer until the transition finishes.

[popover] {
  opacity: 0;
  transition: opacity 200ms, display 200ms allow-discrete, overlay 200ms allow-discrete;
}

[popover]:popover-open {
  opacity: 1;
}

@starting-style {
  [popover]:popover-open {
    opacity: 0;
  }
}

The same block works for dialog[open], and ::backdrop needs its own copy of it if you fade the backdrop too.

Reach for the popover attribute first. Most overlays in a typical application are menus, panels and tooltips, and every one of those is smaller, faster and more accessible as a popover than as the div-plus-listener stack it probably is today. Keep showModal() for the handful of moments where the user genuinely cannot carry on until they answer, and treat a non-modal <dialog> as a sign you wanted a popover.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch