Popover API — Why You Can Now Build Tooltips Without JavaScript
A single popover attribute handles open/close, Esc dismissal, and accessibility automatically
After this you can open and close tooltips, menus, and toasts without JavaScript, and pin them right under their button. Plus why modals belong to dialog, not popover.
<button popovertarget="tip-1">Help</button>
<div id="tip-1" popover="auto" role="tooltip">
This is a native tooltip
</div>
Zero lines of JavaScript. No event listeners. No state tracking. Yet it dismisses with Esc, supports keyboard navigation, and screen readers announce it correctly.
One thing up front: this opens when the button is pressed (Enter and Space count). A tooltip that appears on mere hover still needs JavaScript — that comes back in "Where JavaScript Is Still Needed" below.
How Traditional Tooltips Break
"Show on hover, hide on leave" sounds simple. Deploy it and problems pile up.
Keyboard users Tab to the trigger — nothing appears. Screen readers read it twice or not at all. Fast mouse movement causes flickering. Small screens cause overlap. Esc doesn't dismiss. Focus gets lost.
Event listeners accumulate. Separate hover/focus handling. Outside-click edge cases. Manual ARIA synchronization. The code bloats. Not because the implementation is bad, but because the web platform had no concept of "this element is a tooltip."
What Popover API Changed
Add the popover attribute and the browser understands the element's role. Three things work automatically.
1. Open/close without JavaScript
popovertarget="id" connects button to popover. Done. The browser handles opening and closing on click (Enter and Space included). popovertargetaction specifies behavior — show, hide, or toggle (default).
2. Automatic Esc handling
No global keydown handler. No Esc-specific cleanup logic. The browser recognizes popovers as dismissible elements.
3. Automatic ARIA sync
The browser tells screen readers whether the trigger is expanded or collapsed. It doesn't add an aria-expanded attribute to the DOM — it happens in the accessibility tree. So there's no attribute for JS to keep in sync, and nothing to drift out of sync.
The z-index Fight Is Over — Top Layer
An open popover renders in the top layer, a separate layer above everything else on the page. It sits above an element with z-index: 9999, and a parent's overflow: hidden doesn't clip it.
If you've ever moved a dropdown's DOM to the end of body because its card kept cutting it off, this is the part you'll like most.
Want to dim what's behind it? Use ::backdrop.
[popover]::backdrop {
background: rgb(0 0 0 / 0.3);
}
It Doesn't Trap Focus — Use dialog for Modals
While a popover is open, Tab still reaches elements outside it. It doesn't block the page behind it either (no focus trap).
If you need "finish this before doing anything else", like a confirm box, open a <dialog> with showModal(). That makes the rest of the page inert.
The rule of thumb: tooltips, menus, and toasts are popovers; a window the user must answer before moving on is a modal dialog.
auto vs manual
<!-- auto: closes on outside click -->
<div popover="auto">auto</div>
<!-- manual: closes only when you say so -->
<div popover="manual">manual</div>
auto closes on outside click, Esc, or when another auto popover opens. Good for tooltips.
manual ignores outside clicks and Esc. You close it with a button (popovertarget) or hidePopover(). In exchange, several can be open at once — handy for toast notifications.
Several Buttons Can Open One Popover
popovertarget just points at an id, so any number of buttons can point at the same one. A "?" in the header and a button in the body can open the same help panel. popovertarget only works on <button> (and <input type="button">) — put it on an <a> and nothing happens.
Split open-only and close-only buttons with popovertargetaction: show only opens, hide only closes, and the default toggle closes it if it's open.
<button popovertarget="help">?</button>
<button popovertarget="help" popovertargetaction="show">Show help</button>
<div id="help" popover>
...
<button popovertarget="help" popovertargetaction="hide">Close</button>
</div>
Positioning in CSS — the Button Is the Anchor
The first time you open a popover, it shows up dead center. The default style is position: fixed + margin: auto. A tooltip needs to sit next to its button.
Wiring a button with popovertarget makes it the popover's anchor (the element you position relative to). So without any anchor-name, one position-area line puts it right under the button.
[popover] {
margin: 0;
position-area: bottom;
}
Don't forget margin: 0. If the default margin: auto stays, the position goes off. If you use anchor() or anchor-center for finer placement, add position-anchor: auto to opt into this implicit anchor (per MDN).
Browsers without CSS anchor positioning ignore the line and center the popover again. If that would break your UI, see the library section below.
Where JavaScript Is Still Needed
Timing control. Native popovers open/close instantly. Fast mouse movement still causes flicker. Delay between hover and open requires JavaScript.
Hover intent. The browser can't know why a pointer is over an element — intentional hover or passing through. JavaScript handles this.
But the core shifted. JavaScript no longer owns the entire interaction model. Popover API handles base open/close; JavaScript just layers intent detection on top.
When Libraries Still Win
Complex positioning with nested scroll containers and custom flipping logic — Floating UI still wins here. Plain "under the button" is covered by position-area above, but if browsers without anchor positioning must get the same placement, a library is still easier.
Large design systems needing centralized behavior and guardrails across teams.
Teams new to accessibility. Popover API gives good defaults, but you still need to know when to add a role, a label, or focus handling.
For most cases though — simple tooltips, dropdown menus, toast notifications — Popover API is enough. No library needed.
If you're swapping hand-rolled tooltips and modals in an existing app for popover and dialog and getting stuck, feel free to ask negabaro.
Key Points
popover + popovertarget handles open/close, Esc, keyboard navigation automatically
The browser reports expanded state in the accessibility tree — no ARIA attribute for JS to sync
Open popovers render in the top layer — no z-index or overflow: hidden clipping. Need a modal with a focus trap? Use dialog.showModal()
auto closes on outside click/Esc and only one shows at a time; manual closes only via a button or hidePopover(), and several can stay open
The popovertarget button is an implicit anchor — position-area: bottom puts the popover under it (remember margin: 0)
Timing control and hover intent still need JavaScript, but the core interaction model is browser-owned