DEV Community

Anja Beisel
Anja Beisel

Posted on

The HTML popover Attribute — Complete Deep Dive

The popover attribute is a modern, built-in way to create lightweight overlays like:

  • dropdowns
  • menus
  • tooltips
  • context panels
  • mini-dialogs

It is native HTML, meaning:

  • no JavaScript library required
  • no ARIA hacks required
  • no portal logic
  • minimal focus management (mostly handled for you)

It was added as part of the Open UI / HTML Living Standard and is now supported in modern Chromium, Safari, and Firefox versions.


Why this matters

For years, building dropdowns, menus, and overlays meant:

  • custom JS state management
  • focus traps
  • event listeners everywhere
  • z-index bugs
  • accessibility edge cases

The Popover API replaces most of that with native browser behavior.

This is one of the biggest shifts in UI development since <dialog>.


Popover vs Dialog vs Div

Feature Popover Dialog Div + JS
Built-in open/close
Light dismiss ❌ (modal only)
Top layer
Focus management Partial Strong Manual
Positioning Needs anchor Centered Flexible
Best for Menus, dropdowns Modals Custom layouts

Basic Concept

A popover is a hidden element that can be shown/hidden declaratively via attributes. You declare

<div popover id="menu">
  Hello Popover
</div>
Enter fullscreen mode Exit fullscreen mode

The element is hidden by default. To show it, connect it to a button:

<button popovertarget="menu">Open</button>
Enter fullscreen mode Exit fullscreen mode

That’s it. No JS required.


How it works internally

When a popover is opened, it is removed from display: none, enters the top layer (like <dialog>) and so renders above all stacking contexts and behaves independently of z-index, and is not clipped by overflow: hidden.


popover Attribute Values

<div popover="auto"> or <div popover> (default)

Behavior

  • clicking outside closes it
  • pressing ESC closes it
  • only one auto popover open at a time

This is ideal for menus, dropdowns, tooltips, context panels

<div popover="manual">

Behavior

  • does NOT close automatically
  • you must close it manually via JS
  • multiple can be open simultaneously

Use this when you need custom open/close logic, you're building complex UI.


How To Trigger a Popover

Declarative Trigger

<button popovertarget="myPopover">
  Open
</button>

<div popover id="myPopover">
  Content
</div>
Enter fullscreen mode Exit fullscreen mode

You can control the toggle behavior with popovertargetaction with possible values toggle (default), show or hide.

<button
   popovertarget="myPopover"
   popovertargetaction="show">
Enter fullscreen mode Exit fullscreen mode

JavaScript API

You can control popovers manually

const pop = document.getElementById("myPopover");

pop.showPopover();
pop.hidePopover();
pop.togglePopover();

// check popover state
pop.matches(":popover-open");
Enter fullscreen mode Exit fullscreen mode

Styling Popovers

The default styling depends on the browser. For example in Chrome you see

[popover]:where(:not(:popover-open)) {
   display: none;
}

[popover]:popover-open {
   display: block;
   overlay: auto !important;
}

[popover] {
   position: fixed;
   position-anchor: auto;
   width: fit-content;
   height: fit-content;
   color: canvastext;
   background-color: canvas;
   inset: 0px;
   margin: auto;
   border-width: initial;
   border-style: solid;
   border-color: initial;
   border-image: initial;
   padding: 0.25em;
   overflow: auto;
}
Enter fullscreen mode Exit fullscreen mode

The Popover is in the top layer, with fixed position, and centered on the visible screen. A simple way to style it is to leave its positioning, inset, and margin alone.

[popover] {
   border: none;
   border-radius: var(--radius-xl);
   box-shadow: var(--shadow-lg);
   padding-block: calc(var(--spacing) * 3);
   padding-inline: calc(var(--spacing) * 3);
   width: max-content;
   max-width: 40rem;
}
Enter fullscreen mode Exit fullscreen mode

To add an effect when the Popover opens, use the pseudo class :popover-open like in

[popover].popIn:popover-open {
   animation: menuPopIn 0.2s;
}

@keyframes menuPopIn {
   0% {
      opacity: 0;
      transform: scale(0.9, 0.9);
   }
   60% {
      opacity: 1;
      transform: scale(1.03, 1.03);
   }
   100% {
      transform: scale(1, 1);
   }
}
Enter fullscreen mode Exit fullscreen mode

The open Popover has a ::backdrop pseudo-element which you can style

[popover].showBackdrop::backdrop
{
   background-color: var(--blue-500-10);
   backdrop-filter: blur(8px);
}
Enter fullscreen mode Exit fullscreen mode

Popover positioning

As mentioned before, a popover is opened on the top layer, position fixed and centered.

But what if I want a popover for example positioned relative to the button which opens it?

.popover-wrapper {
   position: absolute;
   top: 0;
   left: 0;
}
Enter fullscreen mode Exit fullscreen mode
<div class="relative">
   <button type="button" popovertarget="popover3">Open popover</button>
   <div id="popover3" popover="auto" class="popover-wrapper">
      ...
   </div>
</div> 
Enter fullscreen mode Exit fullscreen mode

As you've probably guessed, the popover will not behave like a normal absolutely positioned element inside a relative parent. Popover elements are rendered in the top layer. They are removed from normal DOM stacking. They are not positioned relative to their DOM parent. It will appear at Top-Left of the viewport.

If you want to position a popover relative to another element, you have three real options

  • anchor Positioning (Best modern solution)
  • JS positioning

const rect = button.getBoundingClientRect();

popover.style.top = rect.bottom + "px";

popover.style.left = rect.left + "px";

  • don’t use popover

Use a normal <div>. Toggle visibility manually. Then position: absolute works normally.


Anchored Positioning

Modern browser support anchor positioning

button {
  anchor-name: --tabs;
}

[popover] {
  position-anchor: --tabs;
}
Enter fullscreen mode Exit fullscreen mode

This allows true dynamic anchoring without JS.

By assigning an anchor name and then the anchor to the popover with position-anchor, the distance between the anchor and the viewport top and left is available in the styles of the popover.

CSS Popover Anchoring

.anchor {
    anchor-name: --tabs;
}

.anchored-popover {
    position-anchor: --tabs;
    top: anchor(bottom);
    left: anchor(right);
    transform: translateX(-100%);
    margin: 0;
}
Enter fullscreen mode Exit fullscreen mode
<div>
   <button type="button" popovertarget="popover3" class="anchor">Anchor positioned popover</button>
   <div id="popover3" popover="auto" class="anchored-popover">
      <ul>
         <li>Tree</li>
         <li>Add</li>
         <li>Edit</li>
         <li>Assign CSS</li>
      </ul>
   </div>
</div>
Enter fullscreen mode Exit fullscreen mode

No anchor positioning in older browsers: CSS supports check + JS fallback

Popover API and CSS Anchor Positioning are both young, with Popover API being realistically supported by all modern engines since April 2024. CSS Anchor Positioning only recently landed in Firefox (late 2025).

Though you could demand that your users work with modern browser only, there is a safer way and it is not hard to set up.

Use CSS @supports to check if your browser supports CSS anchoring

@supports (anchor-name: --a) {
   .anchor { 
      anchor-name: --tabs; 
   }

   .anchored-popover {
      position-anchor: --tabs;
      top: anchor(bottom);
      left: anchor(right);
      transform: translateX(-100%);
   }
}
Enter fullscreen mode Exit fullscreen mode

If CSS anchoring is not supported, add a JS fallback, here one way to do it with a React Custom Hook

const supportsAnchors = (): boolean => {
   return !!CSS?.supports?.("anchor-name: --a");
};

export const useGetAnchorStyles = ({
   anchorName,
   pos,
   offSetH,
   offSetV,
   popoverRef,
   anchorRef,
}: {
   anchorName: string;
   pos: TPos;
   offSetH?: string;
   offSetV?: string;
   popoverRef: RefObject<HTMLElement | null>;
   anchorRef: RefObject<HTMLElement | null>;
}) => {
   const [styles, setStyles] = useState<CSSProperties>({});

   useEffect(() => {
      const pop = popoverRef.current;
      const anchor = anchorRef.current;
      if (!pop || !anchor) return;

      const handleToggle = (e: Event) => {
         const toggleEvent = e as ToggleEvent;
         const isOpen = toggleEvent.newState === "open";

         if (isOpen && !supportsAnchors()) {
            const nextStyles = getPopUpPositionRelativeToAnchor({
               pos,
               offSetH,
               offSetV,
               popoverRef,
               anchorRef,
            });
            setStyles(nextStyles);
         }
      };
      pop.addEventListener("toggle", handleToggle);

      return () => {
         pop.removeEventListener("toggle", handleToggle);
      };
   }, [popoverRef, anchorRef, pos, offSetH, offSetV]);

   useEffect(() => {
      if (supportsAnchors()) {
         const nextStyles = getAnchorStyles({
            anchorName,
            pos,
            offSetH,
            offSetV,
         });
         setStyles(nextStyles);
      }
   }, [anchorName, pos, offSetH, offSetV]);

   return styles;
};
Enter fullscreen mode Exit fullscreen mode

And then in your component you set the styles generated by useGetAnchorStyles on the popover which results, depending on the make and age of the browser of the user, in a relative position of the top left corner of the popover as achor() or in pixel.

React style example

// browser does support anchor positioning
{
   "positionAnchor": "--html-tree-anchor",
   "top": "anchor(bottom)",
   "left": "anchor(left)"
}
// browser does not support anchor positioning
{
   "left": "73.5px",
   "top": "321.25px"
}
Enter fullscreen mode Exit fullscreen mode

While you are here: Fix Popovers Above <iframe> with pure CSS

When using the Popover API, the browser normally closes a popover automatically when the user clicks outside it. This is called light dismiss and works out of the box with:

<div popover="auto">...</div>
Enter fullscreen mode Exit fullscreen mode

However, there is one common case where this breaks. If a popover sits above an <iframe>, clicking on the iframe does not close the popover. Why? Because an iframe is a separate browsing context. Pointer events inside it do not reach the parent document. So when the popover is open, user clicks iframe, event handled inside iframe document, parent page never receives the outside click and popover stays open.

The modern CSS solution

You can disable pointer interaction with the iframe whenever a popover is open:

body:has(:popover-open) iframe {
  pointer-events: none;
}
Enter fullscreen mode Exit fullscreen mode

How it works

This uses three modern CSS features :has(), :popover-open and pointer-events: none. :has() allows a parent element to react to the state of its descendants. body:has(:popover-open) means "if any element in the document currently has an open popover". pointer-events: none removes the element from pointer hit-testing. Now when a popover is open CSS disables pointer events on iframe. When the user clicks outside the popover in the iframe area, the click goes to the parent page and the browser performs automatic popover light-dismiss, means the popover closes.

This technique is useful when you have UI elements above an iframe, such as preview panels, embedded demos, code playgrounds, editors with a rendered output, and dashboards with embedded content.

This tiny CSS snippet is super clever. I did discuss my initial problem, can't close popovers with click over iframe, with ChatGPT. There were some long and complex suggestions where and how to attach event listeners, or add manual overlays, checking the internet, and answering patiently my requests for a simple solution. CSS and HTML become really clever, but we are not used to it. I keep the one above as my little spell, which shall guide me to think straight about a modern HTML and CSS solutation first, and plan the JS fallback really just if I must.


Common gotchas

  • Popovers ignore parent positioning (top layer)
  • margin: auto centers them by default
  • Clicks inside iframes don’t close them
  • Anchor positioning is not fully supported everywhere yet

Final thought

The Popover API is not just a new feature — it’s a shift.

It moves UI patterns from JavaScript into the browser itself.

Fewer bugs. Less code. Better accessibility.


If you want to explore more modern HTML, CSS, and SVG patterns,

you can find interactive examples on CSSEXY.com.

Top comments (0)