The popover Attribute: Top-Layer Overlays With No JavaScript
Overlay components have a reputation for being the buggiest thing in any component library, and the reputation is earned. A dropdown clipped by an ancestor's overflow: hidden. A modal that sits behind a sticky header because a parent created a stacking context. An outside-click handler bound to document that fights with a stopPropagation call three components away. An Escape listener that closes two panels at once. None of that is essential complexity — it is all consequence of building overlays inside the normal document. The popover attribute moves them out of it. This page, part of the CSS anchor positioning and overlays section, covers what the attribute gives you for free, how ::backdrop works, and how to animate entry and exit with @starting-style and transition-behavior: allow-discrete — all in HTML and CSS, with no script anywhere.
Why this instead of a hand-rolled overlay
Two attributes replace a component's worth of behaviour:
<button popovertarget="panel">Open</button>
<div id="panel" popover>Hello</div>
That markup already gives you a toggle button, an element promoted to the top layer when shown, light dismiss on an outside click, close on Escape, focus returned to the invoking button on close, and an implicit relationship between the button and the panel exposed to assistive technology. It also gives you correct behaviour in cases people usually forget: opening a second popover closes the first, unless the second is nested inside it; and the browser's dismissal handling knows about the top-layer stack, so nested popovers unwind one level at a time rather than all at once.
The reason to prefer this to a bespoke implementation is not that the code is shorter. It is that the tricky parts are handled by the same engine that owns focus, the stacking order, and the event loop. An outside-click handler in JavaScript is an approximation of light dismiss; the browser's version is the definition of it.
When not to use it: anything requiring a decision before the user proceeds. Popovers are non-modal by design — content behind them remains interactive and focus is not trapped. For a confirmation prompt or a blocking form, use <dialog> with showModal(), which does trap focus and marks the rest of the page inert.
The three states, and the two discrete properties
Everything about styling a popover follows from one fact: a hidden popover is display: none. It is not transparent, not scaled to zero, not moved off-screen — it does not generate a box at all. That is what makes it cheap and what makes animating it awkward, because there is no box to animate from.
The open state is selectable with :popover-open, which matches only while the element is showing. The two properties that change across the boundary — display and overlay — are discrete: they have no intermediate values, so by default they flip at the very start of a transition. That is fine going in and catastrophic coming out, because on close the element hits display: none on frame one and there is nothing left to fade.
transition-behavior: allow-discrete changes the flip point for those properties to the end of the transition instead of the start. @starting-style supplies the missing "before" value for the entry, since an element appearing from display: none has no previous computed style to interpolate from. You need both, and you need overlay in the list as well as display — overlay is the property that represents top-layer membership, and if it is left out the panel drops behind other content the instant it starts closing.
Complete working implementation
Open the panel below, then click outside it or press Escape. Nothing in the frame is scripted.
<button type="button" popovertarget="menu" class="pop-btn">Open panel</button>
<div id="menu" popover class="pop">
<h3 class="pop__title">Export</h3>
<p class="pop__body">Click anywhere outside this panel, or press Escape, to dismiss it.</p>
<button type="button" popovertarget="menu" popovertargetaction="hide" class="pop-btn">
Close
</button>
</div>
.pop {
max-width: 20rem;
padding: 1rem;
border: 1px solid #d3d8e0;
border-radius: 0.75rem;
background: #fff;
/* Closed appearance. The element is display:none here, so these values
only matter as the target the exit transition runs towards. */
opacity: 0;
translate: 0 -6px;
/* display and overlay must both carry allow-discrete or the exit is
cut off on frame one. */
transition:
opacity 0.2s ease,
translate 0.2s ease,
overlay 0.2s allow-discrete,
display 0.2s allow-discrete;
}
/* Open appearance. :popover-open matches only while the panel is showing. */
.pop:popover-open {
opacity: 1;
translate: 0 0;
}
/* The frame before the open state exists. Without this the panel would
simply appear at full opacity — there is no prior value to animate from. */
@starting-style {
.pop:popover-open {
opacity: 0;
translate: 0 -6px;
}
}
/* Every top-layer element gets a ::backdrop, rendered directly beneath it. */
.pop::backdrop {
background: rgb(0 0 0 / 0.45);
opacity: 0;
transition:
opacity 0.2s ease,
overlay 0.2s allow-discrete,
display 0.2s allow-discrete;
}
.pop:popover-open::backdrop { opacity: 1; }
@starting-style {
.pop:popover-open::backdrop { opacity: 0; }
}
.pop-btn {
font: inherit;
padding: 0.5rem 0.9rem;
border: 1px solid #d3d8e0;
border-radius: 0.5rem;
background: #fff;
}
Note the second popovertarget on the close button, paired with popovertargetaction="hide". Without the action attribute the button would toggle, which on an already-open popover works but reads ambiguously; being explicit means the same button can never accidentally reopen a panel that some other interaction just closed. The available actions are toggle (the default), show, and hide.
The technique: ::backdrop is a sibling of the top layer, not of the page
::backdrop is easy to misread as "a dimming div the browser inserts for you." It is more specific than that, and the specifics decide what you can do with it.
It is generated for every element in the top layer, and it paints immediately below that element and above everything else in the document. It is not a child of the popover and it is not a child of the body — it sits in the top layer alongside its originating element, which is why no z-index anywhere in your stylesheet can get above or between them. It inherits nothing from the popover, so it needs its own transition declarations rather than picking them up from the parent rule; this is the single most common reason a panel fades nicely while its backdrop snaps off.
It also does not intercept anything you need to handle. Light dismiss is implemented by the browser at the popover level, not by a click on the backdrop, so you get outside-click dismissal whether or not you style ::backdrop at all — and styling it does not break that behaviour the way an inserted overlay div would.
One practical consequence: because a popover="auto" element is non-modal, a dark backdrop over the whole viewport can be actively misleading. It looks modal, but clicks still reach the content behind it and screen reader users can still navigate there. Reserve a full dimming backdrop for genuinely modal <dialog> elements, and use something lighter — a subtle scrim, or nothing at all — for popovers.
Variation: an auto popover versus a manual one
The attribute's value changes the dismissal model, and picking the wrong one produces bugs that look like framework problems.
<!-- Default. Light dismiss + Escape; opening this closes other auto popovers. -->
<div id="menu" popover="auto">…</div>
<!-- No light dismiss, no Escape, no auto-close of siblings. -->
<div id="toast" popover="manual">…</div>
popover="auto" — which is what the bare popover attribute means — participates in a dismissal stack. Opening one auto popover closes any other open auto popover that is not one of its ancestors, so nested submenus work but two unrelated dropdowns can never be open at once. That is the right model for menus, and exactly the wrong model for a stack of notification toasts, where popover="manual" keeps each one alive until something explicitly hides it.
For reduced-motion users, drop the movement but keep the discrete transitions in place — the fade duration is what keeps the exit visible at all, so removing the whole transition declaration would make the panel vanish abruptly rather than gently:
@media (prefers-reduced-motion: reduce) {
.pop {
translate: none;
transition:
opacity 0.1s linear,
overlay 0.1s allow-discrete,
display 0.1s allow-discrete;
}
.pop:popover-open { translate: none; }
}
Browser support
The popover attribute is supported in Chrome 114+, Edge 114+, Safari 17+, and Firefox 125+ — the oldest and most widely deployed piece of the overlay stack. transition-behavior: allow-discrete landed in Chrome 117+, Edge 117+, Safari 17.4+, and Firefox 129+; @starting-style matches it everywhere except Safari, where it arrived one release later in 17.5. In an engine that lacks the animation half, a popover still opens and closes correctly — it simply appears and disappears instantly, which is an acceptable degradation and needs no @supports guard. The placement rules that tether the panel to its button depend on anchor positioning, which is also available in every current engine (Chrome/Edge 125+, Safari 26+, Firefox 147+) but shipped recently enough to be worth guarding for older versions; anchor positioning fallbacks covers how to layer that on.
FAQ
What is the difference between popover="auto" and popover="manual"?
An auto popover light-dismisses on an outside click or Escape and closes any other auto popover that is not its ancestor. A manual popover does neither, so it stays open until something explicitly toggles it and it can coexist with other open popovers.
Why does my popover disappear instantly instead of fading out?
Because display and overlay are discrete properties. Without transition-behavior: allow-discrete on both, the element flips to display: none and leaves the top layer on the first frame of the exit, so there is nothing left to fade.
Does a popover trap focus like a modal dialog?
No, and that is intentional. Popovers are non-modal, so focus can leave them and content behind them stays interactive. Use a <dialog> element opened with showModal() for anything that requires a decision before the user continues.
Can I style the area behind a popover?
Yes, through the ::backdrop pseudo-element, which every top-layer element gets. It renders directly beneath the popover and above everything else, and it can be transitioned as long as allow-discrete is included so it survives the exit.
Related
- CSS anchor positioning and overlays — the parent guide, including how the top layer and anchoring combine.
- Anchor positioned tooltips — the lighter component for passive hints.
- Anchor positioning fallbacks — tethering the panel on browser versions older than the anchor ship dates.
- Transitioning display with allow-discrete — cross-area: the discrete transition rules in full.
- @starting-style entry animations — cross-area: more patterns for elements appearing from nothing.
Related articles
More pages in the same section.