The Popover API is a powerful addition to web development, enabling developers to create lightweight, dismissible UI elements with ease.
This native feature simplifies popover creation without requiring JavaScript event listeners or managing focus manually. However, when working with frameworks like Next.js, developers often rely on React state management and hooks like useEffect to handle similar functionality.
In this article, we will explore the Popover API, compare it with traditional approaches, and discuss how it contrasts with React-based implementations.
Key Features of the Popover API
The Popover API introduces several useful capabilities:
Promotion to the top layer: Popovers automatically appear above other content without requiring CSS z-index adjustments.
Light-dismiss functionality: Clicking outside a popover closes it and returns focus to the triggering element.
Built-in focus management: When a popover opens, focus is automatically moved to an interactive element inside it.
Keyboard accessibility: Pressing the Esc key or toggling the popover button twice will close it.
Declarative binding: The popover attribute enables a semantic connection between the trigger and the popover element.
Implementing a Basic Popover
Creating a popover is straightforward using HTML attributes. Here’s a simple example:
<button popovertarget="my-popover">Open Popover</button>
<div id="my-popover" popover>
<p>I am a popover. Press <kbd>Esc</kbd> or click outside to close.</p>
</div>
In this example, the button has a popovertarget
attribute pointing to the id
of the popover.
The div
containing the popover has the popover
attribute.
The popover follows light-dismiss behavior by default.
Different Types of Popovers
By default, the popover behaves as popover="auto"
, which allows automatic dismissal. However, you can explicitly define popover behavior:
Manual Popovers
Manual popovers require explicit closing actions:
<button popovertarget="manual-popover">Open Popover</button>
<div id="manual-popover" popover="manual">
<p>This popover won’t close automatically.</p>
<button popovertarget="manual-popover" popovertargetaction="hide">Close</button>
</div>
Here, the popover remains open until the user clicks the close button, preventing automatic dismissal.
Popover vs. Modal Dialogs
At first glance, popovers and modal dialogs appear similar, but they serve different purposes:
Feature | Popover | Modal Dialog |
---|---|---|
Open Method | popovertarget attribute |
dialog.showModal() |
Close Method | popovertargetaction="hide" |
dialog.close() |
Light-Dismiss | Yes | No |
Blocks Interaction | No | Yes (makes background inert) |
Styling | :popover-open |
[open] attribute |
While popovers work well for tooltips, dropdowns, and inline help messages, dialogs are more suited for blocking interactions like confirmation prompts.
The React Approach
When working with React, developers often use a mix of useStateand useEffect (or something similar) to handle popovers. Since HTML popovers are not yet widely supported across all browsers, it has become fairly standard-practice for React developers to create controlled popovers in this way.
It usually goes a little something like this:
import { useState } from 'react';
export default function PopoverComponent() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Open Popover</button>
{isOpen && (
<div className="popover">
<p>I am a popover. Click outside to close.</p>
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
)}
</div>
);
}
In principal, there’s no issue with this approach, but aside from the fact it doesn’t include the light-dismiss behaviour, it took a lot more code to achieve the same basic result.
Now if we were to add a light-dismiss behaviour, we were tag our modal wrapper with a useRef of popoverRef
, and check if the user has clicked outside of the modal element in a useEffect, and set the isOpen
to false
if they have clicked outside of it.
useEffect(() => {
function handleClickOutside(event) {
if (popoverRef.current && !popoverRef.current.contains(event.target)) {
setIsOpen(false);
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
Developers themselves can’t be blamed for doing this, because it made sense as a workaround for a the lack of support for the popover API. So, now that it’s here, it’s worth trying out, right?
Which Approach Should You Use?
We recommend using the Popover API if:
- You want a declarative approach with built-in light-dismiss behavior.
- You aim for minimal JavaScript and improved performance.
- You want to support the web platform
Use React state solutions:
- You need more control over popover behavior, animations and styling.
- You are working in a React/Next.js environment where component reactivity is key, and these approaches are more common-place (so more readable and recognisable for other team members).
- You need to support older browsers that lack Popover API support.
Conclusion
The Popover API simplifies UI development by introducing a declarative way to create accessible, dismissible elements. While it reduces JavaScript complexity, React developers are accustomed to managing UI state explicitly using hooks.
Depending on your project’s needs, you may opt for the native Popover API or stick with controlled React components. Either way, understanding both approaches will help you build more maintainable and accessible user interfaces.