The Div-and-Script Era
Every dropdown menu on the web has been built on roughly the same foundation for a decade. A positioning engine — Popper.js, then Floating UI — to calculate where the popover should sit relative to its trigger. A click listener on document for light-dismiss. An invisible full-screen overlay div to intercept outside clicks. Manual aria-expanded toggling on the button. A keydown listener watching for Escape. Bootstrap lists Popper.js as a hard dependency just to position a dropdown.
None of that is complex logic. It is all browser-behaviour simulation: your code re-implementing what a proper platform primitive would give you for free. The platform primitive shipped. The Popover API landed in Chrome 114 in May 2023, Safari 17 in September 2023, and Firefox 125 in April 2024. It reached Baseline Widely Available status in April 2025. Can I Use puts global coverage at roughly 90%. No polyfill required.
The Minimal Pattern
Two attributes are all you need to wire up a basic popover:
<button popovertarget="account-menu">Account</button>
<ul id="account-menu" popover>
<li><a href="/profile">Profile</a></li>
<li><a href="/billing">Billing</a></li>
<li><a href="/logout">Log out</a></li>
</ul>
The button opens the menu. Clicking anywhere outside closes it. Pressing Escape closes it. Opening a second popover closes the first. All of that is built in. No JavaScript, no event listeners, no external libraries.
The popover attribute (bare, or popover="auto") promotes the element to the top layer when shown — above all stacking contexts in the document. No z-index calculation puts anything above it. popovertarget connects the button to the popover by id and handles toggle behaviour declaratively.
What You Get for Free
Top-layer placement. The popover renders above everything: fixed headers, sticky navigation, other stacking contexts. The z-index arms race is over before it starts.
Light-dismiss. Click outside the popover and it closes. Press Escape and it closes. The browser implements the full close-signal specification, which also covers the Android back button and assistive technology dismiss gestures.
Singleton enforcement. Open a second auto popover and the first closes automatically. This is the expected behaviour for navigation menus and dropdowns, and the browser handles it without any state management on your part.
Tab order restructuring. The browser moves the popover's content to immediately follow the invoking button in the tab order, regardless of where the element sits in the DOM. You can place your popover markup anywhere convenient and the keyboard experience stays correct.
Focus return on close. When the popover closes, focus returns to the button that opened it. This is free — no tracking code, no manual .focus() call.
auto vs manual
popover="auto" (the default when you write bare popover) gives you light-dismiss and singleton behaviour. One auto popover visible at a time.
popover="manual" removes both. Multiple manual popovers can be open simultaneously. Click-outside does nothing. Escape does nothing. You control the lifecycle entirely with showPopover(), hidePopover(), and togglePopover(). The correct pattern for toast notifications: you want to queue multiple messages without any one dismissing another, each on its own schedule.
<div id="save-toast" popover="manual" role="status">
Saved successfully
</div>
<script>
function showToast(message) {
const toast = document.getElementById('save-toast');
toast.textContent = message;
toast.showPopover();
setTimeout(() => toast.hidePopover(), 3000);
}
</script>
The toast pattern requires JavaScript only for the timeout. Top-layer placement, browser rendering, and lifecycle management are still handled by the platform.
Styling and Position
By default, the browser centres popovers in the viewport with margin: auto. For dropdown menus and tooltips, you need to reposition relative to the trigger. The companion spec for this is CSS Anchor Positioning — now Baseline Newly Available across Chrome, Safari, and Firefox. Set an anchor-name on the trigger and reference it in the popover's position rules:
button {
anchor-name: --my-trigger;
}
[popover] {
inset: unset; / Override the default centering /
position-anchor: --my-trigger;
top: anchor(bottom);
left: anchor(left);
margin-top: 4px;
}
For the popover element itself, reset the default browser styles and apply your own:
[popover] {
margin: 0;
padding: 0.5rem 0;
border: 1px solid #e5e7eb;
border-radius: 6px;
box-shadow: 0 4px 12px rgb(0 0 0 / 12%);
list-style: none;
min-width: 160px;
background: #fff;
}
:popover-open targets the popover element when it is showing. Use it alongside @starting-style for entry and exit animations:
[popover] {
opacity: 0;
scale: 0.97;
transition:
opacity 0.15s,
scale 0.15s,
display 0.15s allow-discrete,
overlay 0.15s allow-discrete;
}
[popover]:popover-open {
opacity: 1;
scale: 1;
}
@starting-style {
[popover]:popover-open {
opacity: 0;
scale: 0.97;
}
}
The @starting-style block defines the starting state for the entry transition. Without it, CSS transitions do not fire on the first render after display: none is removed. The allow-discrete value on display and overlay is required to allow exit animations to complete before the popover leaves the top layer.
Popover vs Dialog
The core distinction is modality. <dialog>.showModal() makes the rest of the page inert: focus is trapped inside, keyboard users cannot leave, screen readers cannot navigate to background content. The point of a modal dialog is to demand attention before anything else continues.
A popover is non-modal. The page stays fully interactive while the popover is open. Other elements remain clickable, scrollable, and keyboard-reachable. Click outside and the popover closes (in auto mode), but nothing blocks the underlying page interaction. This is exactly what you want for navigation menus, tooltips, pickers, and notifications.
The previous post in this series covers <dialog> and showModal() in full detail. The rule of thumb: use popover for supplementary UI that does not require a response, and use <dialog>.showModal() when the user must act before continuing.
Accessibility You Must Add
The browser handles aria-expanded on the invoker button and restructures the tab order. It does not infer the semantic role of the popover's content.
The popover attribute carries no implicit ARIA role. You must provide the right role for the pattern:
-
Dropdown action menu:
role="menu"on the container,role="menuitem"on each item,aria-haspopup="menu"on the button. -
Tooltip:
role="tooltip"on the popover,aria-describedbyon the trigger pointing to the tooltip's id. -
Rich overlay:
role="dialog"on the popover,aria-labeloraria-labelledbyfor the accessible name. -
Toast notification:
role="status"for polite announcements,role="alert"for urgent ones.
For role="menu", you also need arrow-key keyboard navigation. The browser does not provide it. That part requires JavaScript.
A Complete Dropdown Example
Here is a fully accessible dropdown menu with keyboard navigation:
<button
popovertarget="account-menu"
aria-haspopup="menu"
>
Account
</button>
<ul id="account-menu" popover role="menu">
<li><a href="/profile" role="menuitem">Profile</a></li>
<li><a href="/billing" role="menuitem">Billing</a></li>
<li><a href="/logout" role="menuitem">Log out</a></li>
</ul>
<script>
document.getElementById('account-menu').addEventListener('keydown', (e) => {
const items = [...e.currentTarget.querySelectorAll('[role="menuitem"]')];
const index = items.indexOf(document.activeElement);
if (e.key === 'ArrowDown') items[(index + 1) % items.length]?.focus();
if (e.key === 'ArrowUp') items[(index - 1 + items.length) % items.length]?.focus();
if (e.key === 'Home') items[0]?.focus();
if (e.key === 'End') items[items.length - 1]?.focus();
});
</script>
The popover attribute handles open, close, top-layer placement, light-dismiss, ESC close, and focus return. The script adds twelve lines of arrow-key navigation. That is the full implementation.
Before this API, the same component required Popper.js for positioning, a separate click-outside handler, manual aria-expanded management, focus trap logic, and an Escape key listener on document. Popper.js is around 7 KB minified and gzipped. Tippy.js, which wraps it for the tooltip and popover UX layer, adds another 3–5 KB. The platform has absorbed the majority of that work.
Start Here
The Popover API is production-ready. Ninety percent global coverage, Baseline Widely Available since April 2025, no polyfill. The MDN Popover API reference has the full specification and the Can I Use browser table tracks current support in detail.
Copy the dropdown example above, remove Popper.js from your dependencies, and move on.
This is the second post in a series on the native web. The browser has been shipping the tools you have been reaching for libraries to get. Start with modals if you haven't already, then come back here for popovers.