Back to Basics: Popovers and the Popover API
I'm continuing my exploration of modern web standards in this "Back to Basics" series. In my previous post about modal dialogs, I showed how the HTML <dialog>
element eliminated the need for positioning libraries. Today, I want to talk about something else that's just as revolutionary: the Popover API
.
The New Way: HTML Popover API
The Popover API is designed specifically for this use case and handles positioning, stacking, and accessibility automatically. You can read more about it in the MDN Popover API documentation.
Here's the same functionality using modern HTML:
<!-- The modern approach --> <button popovertarget="tooltip-1" popovertargetaction="show"> Hover me </button> <div id="tooltip-1" popover="auto"> This is a helpful tooltip </div>
/* Much simpler CSS */ [popover] { background: #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 14px; max-width: 200px; border: none; margin: 0; } /* Optional: Add a subtle animation */ [popover]:popover-open { animation: fadeIn 0.2s ease-out; } @keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
// Minimal JavaScript (optional) const tooltip = document.getElementById('tooltip-1'); const trigger = document.querySelector('[popovertarget="tooltip-1"]'); // Optional: Add hover behaviour trigger.addEventListener('mouseenter', () => { tooltip.showPopover(); }); trigger.addEventListener('mouseleave', () => { tooltip.hidePopover(); });
That's it. No positioning calculations, no viewport detection, no arrow positioning, no z-index management. The browser handles everything.
Try It Yourself
Here's a working example you can interact with:
This popover is powered by the Popover API!
This popover works with just HTML - no JavaScript required. The browser handles the positioning automatically.
Note: The Popover API is great for popovers (like dropdowns, menus, etc.) but for true tooltips that need to be positioned near elements, you might still want to use a positioning library or the CSS :hover
approach with position: absolute
.
CSS Note: The overlay
CSS property (like overlay: auto
) is a newer CSS feature that controls how elements are rendered in the top layer. It's particularly useful for popovers and dialogs, allowing you to control stacking and rendering behaviour. For example: [popover]:popover-open:not(dialog) { overlay: auto !important; }
The overlay
CSS Property
The overlay
property is a newer CSS feature that controls how elements are rendered in the top layer. According to the MDN documentation, it has two possible values:
auto
: The element is rendered in the top layer if it's promoted to the top layernone
: The element is not rendered in the top layer
This is particularly useful for popovers and dialogs because it gives you fine-grained control over how elements stack and render. For example:
/* Ensure popovers render in the top layer */ [popover]:popover-open:not(dialog) { overlay: auto !important; } /* Prevent an element from rendering in the top layer */ .some-element { overlay: none; }
The overlay
property is especially important when working with animations on popovers. When transitioning overlay
, you need to set transition-behavior: allow-discrete
so that it will animate properly. This ensures smooth transitions when elements are promoted to or removed from the top layer.
When to Use Popover vs Dialog
For any avid reader with a keen eye for detail, you might have noticed there's some overlap between these two components. Both can display overlays, but they serve different purposes:
Use <dialog>
for:
- Confirmations and alerts
- Important forms that require full attention
- Content that should block interaction with the page
- Modal dialogs that need focus trapping
Use Popover API for:
- Dropdown menus and navigation
- Contextual help and information
- Non-blocking overlays
- Content that appears near trigger elements
The key difference is blocking vs non-blocking. Dialogs block interaction with the background, while popovers allow users to continue interacting with the page. Choose based on whether you need the user's full attention or just want to provide additional context.
Styling Popovers
The key to styling popovers is to override the browser's default styles. Here's how to do it properly:
/* Override browser defaults */ [popover] { /* Remove default browser styling */ border: none !important; padding: 0 !important; margin: 0 !important; background: transparent !important; /* Let the browser handle positioning */ position: fixed; inset: auto; } /* Style your popover content */ #demo-tooltip { background: #1f2937; color: white; padding: 0.5rem 0.75rem; border-radius: 0.25rem; font-size: 0.875rem; max-width: 12rem; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
The important thing is to override the browser's default [popover]
styles first, then apply your custom styling to the specific element.
What You Get for Free
The Popover API provides a lot of functionality out of the box:
Automatic Positioning
The browser automatically positions the popover to fit within the viewport, avoiding edges and ensuring it's always visible. No more manual calculations or edge case handling.
Proper Stacking
Multiple popovers stack correctly with appropriate z-index values. The browser manages the stacking context automatically.
Accessibility Built-In
- Screen readers announce when popovers appear and disappear
- Proper focus management
- Keyboard navigation support
- ARIA attributes are automatically applied
Light Dismiss
Popovers automatically close when you click outside of them or press the Escape key. No need to manually handle these interactions.
Performance
No JavaScript positioning calculations means better performance, especially on mobile devices.
Popover Types
The Popover API supports different types of popovers. For a complete reference, check out the MDN popover attribute documentation:
<!-- Manual popover (requires explicit show/hide) --> <div popover="manual" id="manual-popover"> This requires explicit show/hide </div> <!-- Auto popover (shows on trigger, hides on light dismiss) --> <div popover="auto" id="auto-popover"> This shows/hides automatically </div> <!-- Hover popover (experimental) --> <div popover="auto" id="hover-popover"> This shows on hover </div>
Advanced Usage: Rich Content Popovers
For more complex popovers with rich content, the API still shines:
<button popovertarget="rich-popover" popovertargetaction="show"> Show Details </button> <div id="rich-popover" popover="auto"> <header> <h3>User Profile</h3> <button onclick="this.closest('[popover]').hidePopover()">Ć</button> </header> <main> <img src="avatar.jpg" alt="User avatar" /> <h4>John Doe</h4> <p>Software Engineer at Tech Corp</p> <button>Send Message</button> </main> </div>
[popover] { background: white; border: 1px solid #ddd; border-radius: 8px; padding: 0; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); max-width: 300px; } [popover] header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid #eee; } [popover] main { padding: 16px; } [popover] img { width: 48px; height: 48px; border-radius: 50%; margin-bottom: 8px; }
Browser Support & Progressive Enhancement
Browser support for the Popover API is growing rapidly. Here's the current compatibility data from Can I Use:
For older browsers, you can detect support and fall back gracefully. The MDN CSS.supports() documentation has more details on feature detection:
// Progressive enhancement if (CSS.supports('popover', 'auto')) { // Use native popover element.showPopover(); } else { // Fallback to custom tooltip implementation customTooltip.show(); }
React Implementation
As I predominantly use React, here's how I'd implement this. For more React-specific patterns, the React documentation on portals can be helpful for complex popover scenarios:
import { useRef, useEffect } from 'react'; function ModernTooltip({ children, content, trigger = 'hover' }) { const tooltipRef = useRef(null); const triggerRef = useRef(null); useEffect(() => { const tooltip = tooltipRef.current; const trigger = triggerRef.current; if (!tooltip || !trigger) return; if (trigger === 'hover') { const showTooltip = () => tooltip.showPopover(); const hideTooltip = () => tooltip.hidePopover(); trigger.addEventListener('mouseenter', showTooltip); trigger.addEventListener('mouseleave', hideTooltip); return () => { trigger.removeEventListener('mouseenter', showTooltip); trigger.removeEventListener('mouseleave', hideTooltip); }; } }, [trigger]); return ( <> <span ref={triggerRef} popovertarget={tooltipRef.current?.id}> {children} </span> <div ref={tooltipRef} popover="auto" className="tooltip"> {content} </div> </> ); } // Usage function App() { return ( <ModernTooltip content="This is a helpful tooltip"> <button>Hover me</button> </ModernTooltip> ); }
Simple and Clean
The Popover API is just simpler. No more positioning libraries, no more edge case handling.
I recently removed Popper.js from a project and saved about 50KB. The code is cleaner, and it performs better since the browser handles the positioning natively.
That's really it. Sometimes the simplest solution is the best one.
What I'm Looking At Next
I've got a few more modern HTML elements on my radar that I want to explore in this series. The <details>
and <summary>
elements look promising for accordion-style components - another thing I've been overcomplicating for years.
I'm also curious about native form validation with HTML5 input types, the <picture>
element for responsive images, and CSS :has()
selector for parent-based styling.
The web platform has evolved a lot in the past few years, and I'm excited to continue exploring what's now possible with native HTML, CSS, and JavaScript.
This is part 2 of my "Back to Basics" series where I'm exploring how modern web standards are making our lives as developers much easier. Check out part 1 about modal dialogs if you missed it.