The Popover API
The native popover attribute gives any element show/hide, light-dismiss, Esc, and the top layer for free — menus and tooltips with almost no JavaScript.
In one line: add popover to an element and popovertarget to a button, and the browser hands you a top-layer overlay with show/hide, click-outside dismiss, and Esc — no JavaScript state machine required.
What it is
A popover is any transient overlay that sits above the page: a menu, a tooltip, a date picker, a small dialog. For years these meant a pile of JavaScript — tracking open state, wiring an outside-click listener, handling Esc, juggling z-index, and trapping focus.
The Popover API moves all of that into the platform. Mark an element with the popover attribute and point an invoker at it with popovertarget, and the browser gives you: a toggle, light-dismiss (clicking outside closes it), Esc to close, and rendering in the top layer so it escapes overflow: hidden and z-index entirely.
Why it matters
It deletes the most bug-prone code in a typical component library. Light-dismiss, Esc handling, and top-layer stacking are exactly the things hand-rolled overlays get subtly wrong — a listener that leaks, an Esc that closes the wrong layer, a dropdown clipped by an ancestor's overflow.
It also pairs with two siblings to complete the overlay story: anchor positioning decides where the popover sits relative to its trigger, and @starting-style lets the entry animate from a real CSS starting state — so you get a tasteful fade/zoom-in without a JS animation library.
See it
Tweak it2
Open the popover and click outside it — that dismiss is the browser's, not ours. Switch between an action menu and a tooltip, and toggle the entry animation.
How it works
Three pieces:
popoveron the overlay element. By default it ispopover="auto": opening one auto-popover closes others, and it supports light-dismiss + Esc. Usepopover="manual"when you want to control dismissal yourself.popovertargeton a<button>makes it an invoker that toggles the popover by id — zero JavaScript for the common case. (popovertargetactioncan force"show"or"hide".)- The top layer. An open popover renders above all page content, so
z-indexwars and clipping ancestors stop mattering.
To animate, transition the popover's opacity/transform and declare the entry starting point inside @starting-style — the browser interpolates from there to the open state. Wrap it in a reduced-motion guard.
Build it
<button popovertarget="menu" aria-haspopup="menu">Actions ▾</button>
<div id="menu" popover role="menu">
<button role="menuitem">Rename</button>
<button role="menuitem">Duplicate</button>
<button role="menuitem">Delete</button>
</div>[popover] {
margin: 0;
border: 1px solid var(--border, #2a2a2a);
border-radius: 0.5rem;
}
@media (prefers-reduced-motion: no-preference) {
[popover] {
opacity: 1;
transform: translateY(0);
transition: opacity 150ms ease, transform 150ms ease;
}
/* The state to animate *from* when it opens. */
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: translateY(-0.25rem);
}
}
}
/* Style the click-outside backdrop the top layer provides. */
[popover]::backdrop {
background: transparent;
}Make it yours
Use the controls beside the demo above to change popover contents and animate entry — each change updates the example live.
Experiment in the playground- Open the menu, then click the empty stage around it — light-dismiss closes it with no listener of yours.
- Switch to the tooltip variant: same machinery, different
roleand contents. - Toggle the animation off to feel the reduced-motion path — the popover still opens, just without the transition.
Reproduce it with an LLM
Reproduce it with an LLM
You are a senior front-end engineer. Build an actions menu using the native Popover API: a trigger `<button popovertarget="menu">` and a `<div id="menu" popover>` containing menu items, so the browser handles show/hide, light-dismiss (click outside), and Esc with no JavaScript. Render it in the top layer and animate the entry with a `transition` plus `@starting-style` for opacity/transform, fully disabled under `prefers-reduced-motion: reduce`. Give the trigger `aria-haspopup` and the menu the right roles, keep keyboard focus order sensible, and provide a small JS enhancement only where the platform can't (e.g. arrow-key roving among items). Return only the HTML, CSS, and minimal JS.
Pitfalls & accessibility
Related
CSS anchor positioning
CSS anchor positioning tethers a floating element to another element — tooltips, popovers, menus — with edge-flipping fallbacks, no JS position math required.
The :has() selector
The :has() relational selector styles an element based on what it contains — the long-wished-for parent selector — replacing a lot of state-tracking JavaScript.