HTML Popover Table
| Piece | What it does | Field note |
|---|---|---|
popover attribute | The whole widget | One attribute = top layer + light dismiss + a11y wiring |
popovertarget on button | The trigger | Declarative open/close - zero JS for the basic popover |
popover=auto vs manual | Dismissal semantics | auto: light-dismiss + only one open; manual: neither |
::backdrop | The dim layer | Style the page behind - top layer beats every z-index |
popovertargetaction=show/hide | Split buttons | Separate open and close triggers - menu-bar pattern |
beforetoggle / toggle | The events | Prevent beforetoggle to veto; toggle to lazy-render |
top layer | The superpower | No z-index war with parents - overflow and transforms stop mattering |
vs <dialog> | Modal vs popover | dialog showModal: modal + focus trap; popover: non-modal light-dismiss |
The popover attribute compresses a whole widget into markup: popover on the element, popovertarget on a button - and you get TOP LAYER rendering (no z-index war: parents with transforms and overflow cannot trap it), light dismissal, focus handling and screen-reader semantics with zero JavaScript.
Bottom line: popover=auto gives light dismissal and single-open enforcement (menus, tooltips, pickers); popover=manual is sticky and stackable (toasts, teaching UI). The toggle event enables lazy rendering, beforetoggle can veto, and placement comes from the anchor positioning pair.
The honest part: the dialog-versus-popover split is modal versus non-modal - dialog's showModal() takes focus, blocks the page and needs explicit close; popover is ambient (light-dismiss, no focus grab). Menus and toasts are popovers; confirmations and wizards are dialogs. Picking wrong is a UX bug, not a technical one.
How to use
- Ship the menu in three lines: <button popovertarget=menu>Open</button> <div id=menu popover=auto>items</div> - declarative open, click-away close, focus handled.
- Render lazily: menu.addEventListener('toggle', e => { if (e.newState === 'open' && !filled) fillMenu(); }) - first open pays the render, page load does not.
- Split the triggers: popovertargetaction=show on the open button, =hide on the close button inside - the menu-bar pattern without JS wiring.
Frequently asked questions
What does the top layer actually solve?
The stacking-context prison. A dropdown inside a card with transform: translate or overflow: hidden gets clipped or buried - z-index fights parents because z-index is relative to the stacking context, and the parent's context wins. Top-layer elements (popovers, modal dialogs) render OUTSIDE every stacking context: no parent transform, overflow or z-index can clip or bury them, and ::backdrop dims the entire page behind. This is why popover-based menus work inside any markup no matter how hostile the ancestor CSS - the class of 'my dropdown is cut off' bugs ceases to exist rather than getting patched.
What is the real difference between popover=auto and popover=manual?
Dismissal philosophy. auto is a transient surface: clicking outside closes it (light dismiss), pressing Esc closes it, and OPENING one auto popover closes other open ones (single-open enforcement - the menu behavior). manual is a sticky surface: no light dismiss, multiple can stack (toast stacks, onboarding callouts that must not vanish on stray clicks). Picking wrong cuts both ways: auto on a toast means it evaporates on any outside click; manual on a menu means users must find the close button. There is also the hybrid pattern - auto popover containing a manual popover - for nested UI like a menu with a pinned submenu.
When is a popover the wrong element versus dialog?
Modal versus non-modal is the split. dialog showModal() grabs focus, traps it (Tab cycles inside), blocks page interaction and demands an explicit dismissal - the contract for confirmations, forms and wizards where the user MUST decide before proceeding. Popover is ambient: no focus grab, page stays interactive, light dismiss - the contract for supplementary UI (menus, pickers, hints). Using dialog for a menu traps keyboard users out of the page; using popover for a confirmation lets users dismiss it accidentally and lose the decision. The one-line test: should the user be able to ignore this and keep working? Ignore-able = popover; must-answer = dialog.
How do the beforetoggle and toggle events drive real popover behavior?
beforetoggle is the veto and prepare point: calling preventDefault() cancels the open (guard rails - 'cannot open while saving'), and it can prepare state before showing. toggle fires after the state change (e.newState: 'open'/'closed') and is the lazy-render hook - filling expensive content on first open instead of page load, exactly like details' toggle. The pair also powers cleanup: on closed, return focus or reset internal state. One caveat: the events fire for BOTH directions, so handlers must branch on newState rather than assuming opens - the same both-directions discipline as matchMedia's change event.