Native HTML Popover API Demo (popovertarget) — Free Snippet

Native HTML Popover API Demo · Modals · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

popover="auto" attribute promotes elements to the browser top layer with zero manual z-index management
Declarative popovertarget attribute wires a trigger button to a panel id with no addEventListener needed for open/close
popovertargetaction="hide" explicitly closes an auto popover from within itself, e.g. after a menu item is chosen
popover="manual" opts a panel out of automatic light-dismiss for hover-controlled tooltip-style UI
Programmatic .showPopover() / .hidePopover() JavaScript methods used for hover and timed-dismiss interactions
:popover-open CSS pseudo-class drives entrance animations without any JS class-toggling
Native beforetoggle and toggle events with event.newState ("open"/"closed") power a live interaction log
::backdrop pseudo-element styles the dimmed layer behind auto popovers, same mechanism used by <dialog>

About this UI Snippet

Native HTML Popover API — popover, popovertarget, the Top Layer, and toggle Events Explained

Screenshot of the Native HTML Popover API Demo snippet rendered live

Building a dropdown menu, tooltip, or toast notification used to require a surprising amount of custom JavaScript: manual z-index management, a click-outside listener attached to document, an Escape-key handler, and careful DOM placement to avoid the panel being clipped by a parent's overflow: hidden. The HTML Popover API, standardized across Chrome 114+, Safari 17+, and Firefox 125+, replaces all of that with two plain HTML attributes: popover on the panel itself, and popovertarget on the button that controls it.

The `popover` attribute and the top layer

Adding popover="auto" (or the bare popover attribute, which defaults to "auto") to any element promotes it to the browser's top layer — the same rendering layer used natively by <dialog> and fullscreen elements, sitting visually above everything else in the document regardless of z-index or any ancestor's overflow/transform/position stacking context. This is the single biggest practical win over a hand-rolled position: absolute dropdown: no more z-index wars, no more panels getting clipped inside a scrollable card, no more manually portaling the element to document.body with a framework.

Declarative wiring with `popovertarget`

The menu example in this demo uses <button popovertarget="menu-popover"> with zero JavaScript required to open it — clicking the button toggles the element whose id matches popovertarget. The popovertargetaction attribute (set to "hide" on each menu item here) explicitly closes the popover when an action is chosen, rather than the default toggle behavior. This declarative HTML-only wiring is a meaningful shift: a working dropdown menu needs no addEventListener calls at all for the open/close mechanics.

`auto` vs `manual`: the light-dismiss behavior

The popover attribute's value controls dismiss behavior. popover="auto" (used by the menu and the toast in this demo) gets automatic light-dismiss: clicking anywhere outside the panel, pressing Escape, or opening another auto popover closes it automatically, and only one auto popover can be open at a time by default. popover="manual" (used by the tooltip) opts out of all of that — it will not close on outside click or Escape, and you are fully responsible for calling .showPopover() and .hidePopover() yourself, which is exactly what this demo's tooltip does on mouseenter/mouseleave and focus/blur.

The `:popover-open` pseudo-class and `beforetoggle`/`toggle` events

While a popover is showing, it matches the :popover-open CSS pseudo-class, which this demo uses to trigger an entrance animation (animation: pop-in 0.16s) — no JavaScript class-toggling needed to drive the transition. On the JavaScript side, every popover fires a beforetoggle event just before its state changes and a toggle event immediately after, both carrying an event.newState property ("open" or "closed") so you can hook cleanup logic, analytics, or animation coordination into the exact moment a popover opens or closes — this demo's event log panel is wired entirely from these two events across all three popovers.

Why this matters for 2025/2026 UI work

Before this API, every component library (Radix, Headless UI, Floating UI) shipped its own JavaScript reimplementation of focus trapping, outside-click detection, and top-layer portaling for popovers and menus. The native API doesn't replace advanced positioning logic (you'll still often pair it with the CSS Anchor Positioning API or a library like Floating UI for smart placement), but it does replace the dismiss-behavior and stacking-context plumbing that used to be boilerplate in every single implementation, shipped and maintained by the browser instead of your bundle.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet into an AI coding assistant like Claude and ask it to walk through exactly what happens, event by event, when you click the menu button — specifically the order in which beforetoggle and toggle fire relative to the DOM update, since that ordering matters if you want to run an exit animation before a popover actually leaves the top layer. It's also worth asking the assistant to explain why the tooltip uses popover="manual" with explicit showPopover()/hidePopover() calls while the menu uses purely declarative popovertarget with no JS at all, and when you'd choose one wiring style over the other. You could also ask it to add CSS Anchor Positioning (anchor-name and position-anchor) to automatically place the tooltip relative to its trigger instead of the manual getBoundingClientRect() calculation currently used in positionTip().

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a demo of the native HTML Popover API in plain HTML, CSS, and JavaScript that teaches popover="auto" vs popover="manual", declarative popovertarget wiring, and the beforetoggle/toggle events.

Requirements:
- A menu-style popover opened by a button using the declarative popovertarget attribute (no JavaScript for open/close), with popover="auto" on the panel and menu items that use popovertargetaction="hide" to close it after a choice, plus automatic light-dismiss on outside click and Escape.
- A tooltip-style popover using popover="manual", shown and hidden entirely via JavaScript .showPopover()/.hidePopover() calls triggered by mouseenter/mouseleave and focus/blur on its trigger button, explicitly not dismissing on an unrelated outside click.
- A toast-style popover using popover="auto" that opens via a JavaScript .showPopover() call on a button click and automatically calls .hidePopover() after a short timeout (roughly 2-3 seconds), while still supporting manual dismissal by clicking outside it before the timer fires.
- Visual styling that relies on the :popover-open CSS pseudo-class to trigger an entrance animation, and the ::backdrop pseudo-element for a dimmed background behind the auto popovers, with no manual z-index values needed anywhere since [popover] elements render in the browser's top layer.
- A live event log panel that attaches beforetoggle and toggle listeners to every popover element and prints a timestamped line reporting the element's id and the event.newState value ("open" or "closed") each time it fires.
- Clear inline comments distinguishing which behaviors come for free from the browser (light-dismiss, top-layer stacking, Escape-to-close) versus which require explicit JavaScript (manual show/hide, auto-dismiss timers).

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Open the auto menu popoverClick "Open menu" — this button has popovertarget="menu-popover" pointing at the panel's id, so the browser opens it with zero JavaScript. Click any menu item (each has popovertargetaction="hide") or click anywhere outside the panel to close it via automatic light-dismiss, since the panel uses popover="auto".
  2. 2
    Hover the manual tooltip triggerHover or keyboard-focus "Hover or focus me" — JavaScript calls tipPopover.showPopover() on mouseenter/focus and tipPopover.hidePopover() on mouseleave/blur. Because this panel uses popover="manual", clicking elsewhere on the page will not dismiss it automatically; only the explicit hidePopover() call does.
  3. 3
    Trigger the auto-dismissing toastClick "Show toast" to call toastPopover.showPopover() programmatically, then a setTimeout calls hidePopover() after 2.5 seconds. Even though it opens via JavaScript rather than a popovertarget attribute, it still uses popover="auto" so it also light-dismisses immediately if you click outside it before the timer fires.
  4. 4
    Watch the live event logEvery popover in this demo has beforetoggle and toggle listeners attached that read event.newState ("open" or "closed") and print a timestamped line to the dark log panel, showing exactly when each lifecycle event fires relative to your interaction, including the brief beforetoggle-then-toggle sequence on every open and close.
  5. 5
    Inspect the top-layer stackingOpen your browser devtools and note that none of the three popover panels have an explicit high z-index in the CSS — they render above the rest of the page purely because [popover] promotes them to the browser's top layer, the same rendering layer used by <dialog> and the Fullscreen API.
  6. 6
    Reuse the pattern for your own menus and tooltipsFor a simple toggle, add popover="auto" to any panel and popovertarget="that-panels-id" to its trigger button — no JavaScript required. For hover-driven or programmatically-timed popovers like the tooltip and toast here, call .showPopover() and .hidePopover() directly from your own event listeners instead.

Real-world uses

Common Use Cases

Dropdown menus and action menus without a component library
Context menus, "more options" kebab menus, and command palettes triggered from a button are the textbook popover="auto" use case: automatic light-dismiss on outside click and Escape, native top-layer stacking, and no z-index conflicts with modals or sticky headers elsewhere on the page.
Hover and focus tooltips with manual control
Tooltips need to stay visible while hovered and disappear on mouseleave without being accidentally dismissed by a click elsewhere on the page mid-interaction — popover="manual" combined with explicit showPopover()/hidePopover() calls on mouseenter/mouseleave gives exactly that control, unlike popover="auto"'s automatic dismiss rules.
Toast notifications and transient status messages
Save confirmations, form-submitted toasts, and undo prompts benefit from the top-layer stacking of popover="auto" so they're never accidentally hidden behind a modal or sticky element, combined with a simple setTimeout calling hidePopover() to auto-dismiss after a few seconds.
Replacing custom dropdown/tooltip JavaScript libraries
Many teams ship Radix UI, Headless UI, or a hand-rolled solution purely to get outside-click detection and top-layer rendering for simple menus and tooltips. For UI that doesn't need advanced smart positioning, the native Popover API covers the dismiss-behavior and stacking logic with substantially less JavaScript, pairing well with the Native Dialog Showcase for the modal side of the same top-layer mechanism.
Teaching the browser top layer and declarative HTML wiring
This demo is a clear teaching tool for the shift toward declarative HTML-driven interactivity — popovertarget and popovertargetaction require no JavaScript for the open/close mechanics, which is a useful contrast to teach alongside the equivalent JS-driven .showPopover()/.hidePopover() API for cases needing custom timing or event logic.
Design-system components needing consistent stacking behavior
Design systems that mix menus, tooltips, toasts, and modals across many pages benefit from all of them sharing the same underlying top-layer rendering guarantee, eliminating an entire category of "this dropdown is behind that sticky header" bugs that used to require careful global z-index scale management.

Got questions?

Frequently Asked Questions

popover="auto" gets automatic light-dismiss: clicking outside the panel, pressing Escape, or opening another auto popover closes it, and only one auto popover is typically open at a time. popover="manual" disables all of that — outside clicks and Escape do nothing, and you must call .hidePopover() yourself. Use auto for menus and toasts, manual for hover tooltips or UI you need to keep open during unrelated page interactions.

Popover elements render in the browser's top layer, the same stacking mechanism used by <dialog> and the Fullscreen API, so they ignore ancestor z-index, overflow, and transform stacking contexts entirely. A popover opened while a <dialog> is showModal()-open will actually stack above the dialog by default, since top-layer elements are ordered by most-recently-added, which is worth testing explicitly if your app combines both.

Every popover fires beforetoggle just before its open/closed state changes and toggle immediately after, both with an event.newState property equal to the string "open" or "closed" (and event.oldState for the previous value). This lets you run cleanup, fire analytics, or coordinate an animation exactly when a popover's visibility actually changes, rather than inferring state from a class name or attribute check.

The Popover API itself does not include smart positioning — by default a popover opens at its normal document position (or wherever CSS places it) rather than automatically anchored next to the trigger. For automatic placement that avoids viewport edges, pair popover with the newer CSS Anchor Positioning API (anchor-name / position-anchor) or a JS positioning library like Floating UI, as this demo does manually for its tooltip via getBoundingClientRect().

The browser automatically manages some accessibility semantics — popover panels get an implicit role and are exposed correctly to the top layer for assistive tech, and Escape-to-close is handled natively for auto popovers. However you should still add appropriate ARIA attributes like aria-haspopup or aria-expanded on the trigger button and aria-describedby (as this demo's tooltip trigger does) to fully communicate the relationship, since the API handles rendering and dismiss mechanics but not the full semantic relationship between trigger and content.