Auto-dismissing popovers with the Popover API
The HTML popover attribute lets you place elements in the top layer and control their visibility with a button or JavaScript. Most popovers support light dismissal, closing when the user clicks or taps outside. But there is no built-in auto-close behavior. That’s something you have to build yourself, which is useful for transient UI like banner notifications similar to new-message alerts on phones.
Consider this example: click “Add to my bookmarks” and a notification appears, then dismisses itself after a short delay.
Building the popover
Using the popover attribute is straightforward: add it to a div and specify the popover type.
<div popover="manual" id="pop">Bookmarked!</div>
A manual popover cannot be light-dismissed, so you must explicitly show, hide, or toggle its visibility using buttons or JavaScript. A semantic HTML button can handle the triggering.
<button popovertarget="pop" popovertargetaction="show">
Add to my bookmarks
</button>
<div popover="manual" id="pop">Bookmarked!</div>
The popovertarget attribute links the button to the popover element, while popovertargetaction declares that the action is to show the popover on click.
Hiding with CSS transitions
The challenge: the button only opens the popover; it does not hide or toggle it. To auto-dismiss, the popover needs to close itself after a delay. The popover cannot be closed with CSS alone, but it can be hidden. Adding a transition creates a visual dismissal effect. In this example, hiding is done by reducing the CSS height property to zero.
The popover attribute can be targeted with an attribute selector:
[popover] {
height: 0;
transition: height cubic-bezier(0.6, -0.28, 0.735, 0.045) .3s .6s;
@starting-style {
height: 1lh;
}
}
When the button triggers the popover, its height starts at the value declared in the @starting-style block (1lh). After the transition-delay (set to .6s here), the height animates from 1lh to 0 over .3s, effectively hiding the element.
This only hides the popover visually; the element is still technically open. Closing it properly requires JavaScript.
Closing with JavaScript
First, select the popover and assign it to a variable:
const POPOVER = document.querySelector('[popover]');
Then use a ResizeObserver to track changes to the popover’s dimensions:
const POPOVER = document.querySelector('[popover]');
const OBSERVER =
new ResizeObserver((entries) => {
if(entries[0].contentBoxSize[0].blockSize == 0)
OBSERVER.unobserve((POPOVER.hidePopover(), POPOVER));
});
Start observing when the popover is shown by the button click:
const POPOVER = document.querySelector('[popover]');
const OBSERVER =
new ResizeObserver((entries) => {
if(entries[0].contentBoxSize[0].blockSize == 0)
OBSERVER.unobserve((POPOVER.hidePopover(), POPOVER));
});
document.querySelector('button').onclick = () => OBSERVER.observe(POPOVER);
The observer detects when the CSS height reaches zero at the end of the transition. At that point, the popover is properly closed using hidePopover(), and the observer stops with unobserve().
This example uses height and ResizeObserver, but any CSS property and an appropriate observer can be paired. Reviewing ResizeObserver and MutationObserver documentation can help you find workable alternatives.
A no-JavaScript fallback
When JavaScript is unavailable, light-dismissible popover types act as a fallback. Keep the popover visible by overriding the styles that hide it, leaving the user to dismiss it by clicking outside the element.
If the popover should only be light-dismissible without JavaScript, place that popover inside a <noscript> element before the manual popover, overriding CSS styles as needed.
<noscript>
<div popover="auto" id="pop">Bookmarked!</div>
</noscript>
<div popover="manual" id="pop">Bookmarked!</div>
<!-- goes where <head> element's descendants go -->
<noscript>
<style>
[popover] {
transition: none;
height: 1lh;
}
</style>
</noscript>
Choosing this approach
An alternative implementation would use setTimeout() in JavaScript to delay closing the popover after the button click, adding a class to trigger the transition — no observer would be needed.
The technique described here is useful when you prefer to manage the delay in CSS itself using @starting-style and transition-delay, with no extra class added. JavaScript reacts to the CSS-driven size change at the time CSS defines it, rather than controlling the timing directly.



