The Popover API
The Popover API gives you a clean way to build floating UI elements — menus, tooltips, action menus, toast notifications — without fighting browser quirks or writing dozens of lines of positioning code. You add a popover attribute to an element, and the browser handles showing it on a top layer above everything else, light-dismissing it when the user clicks outside, and animating it in and out.
It’s one of those APIs that makes you wonder why the web spent twenty years doing this manually.
How It Works
Add popover to any element and it becomes a popover:
<button popovertarget="my-menu">Open Menu</button>
<div popover id="my-menu">Menu content here</div>
That’s all you need for a working popover. No JavaScript required, no z-index calculations, no click-outside listeners.
The popover attribute takes three values:
| Value | Behavior |
|---|---|
auto | Light-dismisses when you click outside; only one can be shown at a time |
hint | Like auto but starts hidden without a trigger; more experimental |
manual | Never auto-dismisses; you control visibility entirely via JS or a control button |
Most use cases want auto — it handles the common “click outside to close” behavior automatically.
Toggling via HTML
The simplest path: a <button> with popovertarget:
<button popovertarget="action-menu" popovertargetaction="toggle">Actions</button>
<div popover="auto" id="action-menu">
<button onclick="share()">Share</button>
<button onclick="edit()">Edit</button>
<button onclick="delete()">Delete</button>
</div>
popovertarget identifies the popover element. popovertargetaction can be "show", "hide", or "toggle" (default). The button shows the popover on click and hides it on the next click.
Toggling via JavaScript
For programmatic control, use the instance methods on any HTMLElement:
const popover = document.getElementById("action-menu");
// Show it
popover.showPopover();
// Hide it
popover.hidePopover();
// Toggle it
popover.togglePopover();
// Check state
console.log(popover.popover); // "auto" | "hint" | "manual" | "" (empty = not a popover)
showPopover() throws if the element is already showing. hidePopover() throws if it’s already hidden. togglePopover() handles both cases without throwing.
The beforetoggle Event
beforetoggle fires before the popover changes state. This is where you can cancel the transition, fetch data before opening, or update UI synchronously:
const popover = document.getElementById("toast");
popover.addEventListener("beforetoggle", (event) => {
if (event.newState === "open") {
console.log("Popover about to open");
// Could call event.preventDefault() to cancel
} else {
console.log("Popover about to close");
}
});
event.newState is "open" or "closed". Call event.preventDefault() in beforetoggle to block the transition.
The toggle Event
toggle fires after the state change completes:
popover.addEventListener("toggle", (event) => {
if (event.target.matches(":popover-open")) {
console.log("Popover is now showing");
startAutoHideTimer();
} else {
console.log("Popover is now hidden");
}
});
You can check :popover-open inside the handler to know the new state. Alternatively, event.newState would be "open" or "closed" if ToggleEvent exposes it.
Light Dismiss
This is the killer feature. With popover="auto", clicking outside the popover automatically closes it. No JavaScript, no event listeners, no manually tracking click coordinates.
This covers:
- Clicking anywhere outside the popover tree
- Pressing Escape (which also fires
beforetoggleandtoggle) - Opening another popover (only one
autopopover can be open at a time)
For popover="manual", none of this happens. The popover stays open until you call hidePopover() or use a control button with popovertargetaction="hide".
Backdrop Styling
When a popover is showing, you can style the area behind it using ::backdrop:
/* Dim the background */
#action-menu::backdrop {
background: rgba(0, 0, 0, 0.4);
}
/* Blur the page behind a tooltip */
#tooltip::backdrop {
background: transparent;
backdrop-filter: blur(2px);
}
The ::backdrop pseudo-element only appears while the popover is showing. It’s the same mechanism that <dialog> uses for its modal backdrop.
Styling the Popover
Use the :popover-open pseudo-class to style a popover when it’s visible:
/* Only visible when showing */
#action-menu {
padding: 1rem;
border: 1px solid #ccc;
background: white;
}
#action-menu:popover-open {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
The difference from just using a class is that :popover-open only matches while the element is in the showing state — no need to manually add/remove classes when toggling.
Showing and Hiding via JavaScript
Putting it together — a toast notification system using popovers:
function showToast(message, duration = 3000) {
const toast = document.getElementById("toast");
// Set message
toast.textContent = message;
// Show it
toast.showPopover();
// Auto-hide after duration
setTimeout(() => {
toast.hidePopover();
}, duration);
}
// Attach to buttons
document.querySelectorAll("[data-toast]").forEach(btn => {
btn.addEventListener("click", () => {
showToast(btn.dataset.toast);
});
});
<button data-toast="Saved!">Save</button>
<div popover="auto" id="toast" style="margin: 0;"></div>
No positioning code. No z-index juggling. The browser places the popover and handles dismissal.
Menu Example
A common pattern — an action menu that opens on click:
const menuButton = document.getElementById("menu-btn");
const menu = document.getElementById("action-menu");
menuButton.addEventListener("click", () => {
menu.togglePopover();
});
// Close when clicking a menu item
menu.addEventListener("click", (e) => {
if (e.target.tagName === "BUTTON") {
menu.hidePopover();
// Handle the action
handleAction(e.target.dataset.action);
}
});
// Close on Escape
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && menu.matches(":popover-open")) {
menu.hidePopover();
}
});
With popover="auto" you don’t strictly need the Escape handler — the browser handles it — but it’s good practice for consistency.
Nesting Popovers
You can nest popovers. A submenu inside a menu:
<div popover="auto" id="menu">
<button popovertarget="submenu" popovertargetaction="toggle">More ▾</button>
</div>
<div popover="auto" id="submenu">
<button>Option A</button>
<button>Option B</button>
</div>
Opening the submenu doesn’t close the parent menu (they’re both auto). Closing the parent closes the submenu too, since it’s no longer in the DOM tree of an open popover.
Popover vs Dialog
The Popover API and <dialog> overlap in some ways but serve different purposes:
| Feature | Popover | <dialog> |
|---|---|---|
| Light dismiss | Auto popovers only | No — always modal unless modal="false" |
| Top layer | Yes | Yes |
| Backdrop styling | ::backdrop | ::backdrop |
showModal() | No | Yes |
| Accessibility | Role guesswork, limited | Full dialog semantics |
| Browser support | Chrome 114+, Edge 114+, Firefox 125+, Safari 17.1+ | Everywhere |
Use popover for non-modal floating UI — menus, tooltips, toasts. Use <dialog> for actual dialogs that should block interaction with the rest of the page.
You can combine them: <dialog popover> gives you dialog semantics with popover light-dismiss behavior.
Feature Detection
Check if the API is available:
if ("showPopover" in HTMLElement.prototype) {
console.log("Popover API supported");
} else {
// Fallback: hide/show with CSS classes
}
Or check for the attribute support in CSS:
@supports (popover: auto) {
/* Use popover */
}
Chrome 114+, Edge 114+, Firefox 125+, Safari 17.1+. Firefox and Safari support landed in 2024.
Full Example: Custom Select
Here’s a popover-based custom select that shows a list of options:
class PopoverSelect {
constructor(buttonEl, options) {
this.button = buttonEl;
this.options = options;
this.popover = this.createPopover();
this.value = null;
this.button.addEventListener("click", () => {
this.popover.togglePopover();
});
}
createPopover() {
const el = document.createElement("div");
el.popover = "auto";
el.innerHTML = this.options
.map(opt => `<button type="button" data-value="${opt.value}">${opt.label}</button>`)
.join("");
el.addEventListener("click", (e) => {
const btn = e.target.closest("button");
if (!btn) return;
this.value = btn.dataset.value;
this.button.textContent = btn.textContent;
el.hidePopover();
this.onChange(this.value);
});
document.body.appendChild(el);
return el;
}
onChange(value) {}
}
// Use it
const select = new PopoverSelect(document.getElementById("country-btn"), [
{ value: "us", label: "United States" },
{ value: "gb", label: "United Kingdom" },
{ value: "de", label: "Germany" },
]);
select.onChange = (value) => {
console.log("Selected:", value);
};
No positioning math, no scroll listeners, no click-outside handlers. The browser handles the hard parts.
See Also
- /guides/javascript-shadow-dom/ — combine with shadow DOM for encapsulated popover internals
- /guides/javascript-navigation-api/ — SPAs often use popovers alongside a router
- /guides/javascript-pwa-guide/ — PWAs can use the Popover API for in-app menus and toasts