Originally published at kunalganglani.com — read it there for inline code, hero image, and live links.
CSS Popover API examples accessibility is one of those search queries that screams “I tried the demo, and then it broke in production.” The Popover API plus CSS Anchor Positioning finally kills a big chunk of JavaScript you used to write (or import) just to place a menu near a button. But if you ship it without a focus plan, keyboard semantics, and a fallback story, you’ll just be replacing one bug class with another.
Key takeaways
- The Popover API is a rendering and dismissal primitive, not a semantic one. You still own keyboard behavior, naming, and focus restore.
- Choose
popover="auto"for lightweight, light-dismiss UI (menus, pickers). Choosepopover="manual"when you must coordinate state or block dismissal. - Use CSS Anchor Positioning for placement and collision handling. Let the browser flip with
@position-trybefore you reach for a JS positioning library. - Don’t cargo-cult
role="menu"orrole="tooltip". ARIA roles can make screen readers worse if the interaction model doesn’t match. - Progressive enhancement is not optional. A decent fallback to
<details>or<dialog>beats a broken popover on older browsers.
If your popover doesn’t have a focus strategy, it’s not “modern CSS.” It’s a future incident report.
Concepts and usage (what Popover is, and what it is not)
The MDN Contributors describe the Popover API as a “standard, consistent, flexible mechanism for displaying popover content on top of other page content.” That’s the key. It’s a mechanism. Not a dropdown component. Not a menu system. Not a tooltip spec.
Here’s what you get for free:
-
Top layer rendering: your popover is promoted above normal stacking contexts. You stop playing
z-indexJenga. -
Built-in open/close control: declarative targeting via HTML attributes, plus imperative methods like
showPopover()andhidePopover(). -
Dismissal behavior (for some types):
autopopovers can be light-dismissed, including closing on outside click andEscape.
Here’s what you don’t get:
- Any guarantee that screen readers announce it the way you intend.
- Menu semantics, arrow-key roving focus, or typeahead.
- Modal behavior or focus trapping. (That’s
<dialog>’s job.)
A good mental model: Popover is a better position: absolute + event soup. It’s the platform telling you, “I’ll handle stacking and lifecycle. You handle UX.”
This is also why the API is a sweet spot for design systems. You can standardize placement, dismissal, and state coordination without importing a floating UI library for every micro-interaction.
HTML attributes (the popover attribute, value, and description)
The Popover API is mostly HTML-first, which is the part I like. A UI primitive you can progressively enhance without a framework-specific abstraction layer.
The popover attribute
Per the MDN Contributors, the popover global attribute “designate[s] an element as a popover element.” In practice: add popover to the panel you want to float.
Value: auto vs hint vs manual
MDN documents three main values:
-
popover="auto"(also the default when you writepopoverwith no value): light dismiss. Outside click andEscapeclose it. Showing oneautopopover generally closes other openautopopovers (except nested ones). -
popover="hint": designed for helper UI. It won’t closeautopopovers when shown, but will close other hints depending on ancestry. -
popover="manual": you control closing. No implicit light-dismiss behavior.
If you’ve been wondering “which should I choose for menus/tooltips?” my production bias is:
-
Menus: start with
autounless you have a reason not to. -
Tooltips: prefer
hintwhen available, because tooltips shouldn’t bulldoze menus. -
Anything resembling a dialog: don’t pretend. Use
<dialog>.
The invoker attributes
The other half is how the trigger opens the popover:
-
popovertarget="id"points at the popover element. -
popovertargetaction="toggle|show|hide"(default istoggle).
This is the “no JavaScript required” path, which matters for progressive enhancement and SSR.
Defaults and overrides
Out of the box, popovers come with UA styles (including display: none while hidden). You override with :popover-open for the shown state, and your own layout styles when hidden.
A useful rule: treat popover content like a small surface with a strong border, shadow, and high contrast focus styling. Popovers are usually used at small sizes, and small surfaces are where focus rings get clipped and color contrast regressions sneak in.
Interfaces (showPopover/hidePopover/togglePopover + events)
When declarative control isn’t enough, you switch to the imperative interface.
-
showPopover()shows a valid popover by adding it to the top layer, per MDN Contributors. -
hidePopover()hides a valid popover by removing it from the top layer and styling it withdisplay: none, per MDN Contributors. -
togglePopover()does what you think it does, per MDN Contributors.
Events matter more than most demos admit:
-
beforetogglefires just before show/hide, and can be prevented. It’s a clean hook for focus placement and “should we open?” checks, per MDN Contributors. -
togglefires just after show/hide, but MDN notes it can be coalesced (multiple toggles delivered as fewer events), which means you shouldn’t build fragile state machines around “I’ll always see N toggles,” per MDN Contributors.
In production, I treat these like:
-
beforetoggle: set up what must be true before paint (initial focus target, aria state updates, analytics). -
toggle: clean up after state changes (restore focus, clear selection, stop observers).
CSS features (styling popovers + Anchor Positioning recipes)
Styling is easy. Placement is where this gets interesting.
Styling popovers
You’ll use:
-
:popover-opento style the open state - Normal selectors for the base state
-
@media (prefers-reduced-motion: reduce)to kill cute animations that make keyboard users nauseous -
@media (forced-colors: active)to ensure borders and focus rings survive Windows High Contrast Mode
A simple pattern that doesn’t betray you later:
- Don’t animate
top/leftplacement. Animate opacity and transform. - Keep
max-inline-sizeunder control. Long labels in menus are how popovers overflow on small screens.
CSS anchor positioning popover example
This is the 2026 unlock: Anchor your popover to a trigger without JS measurements.
Una Kravets, Developer Advocate at Google Chrome, calls CSS Anchor Positioning a “game-changer” because it lets you “natively position elements relative to other elements,” shipping in Chrome 125, and updated syntax in Chrome 129 as names changed (e.g., inset-area → position-area). That context is in Una Kravets.
The practical recipe:
- Give the trigger an
anchor-name. - Give the popover
position-anchor(or use the HTMLanchorglobal attribute to link by id). - Use
position-area(or anchor functions) to place it. - Add
@position-tryfallbacks so the browser can flip when the default would overflow.
You also get a browser-level collision strategy that’s far less janky than your “measure on scroll/resize” handler.
A big gotcha: anchored elements can behave differently inside scroll containers. Test popovers inside sidebars, modals, and nested scrollers. “It worked on the marketing page” is not a bar.
Examples (menus, tooltips, and a non-modal “mini dialog”)
This is the meat: CSS popover API examples accessibility that you can paste into a real app, and then argue about in code review.
Example 1: Menu button popover (accessible keyboard behavior)
Use case: avatar menu, “More actions” kebab menu, filter dropdown.
Recommended defaults
- Use
popover="auto"on the menu panel. - Use a real
<button>as the invoker. - Wire
popovertargetfor no-JS open.
Accessibility behavior to implement
-
Enter/Spaceon the button toggles open (native for button; Popover handles open). -
Escapecloses (Popover handles forauto, but still validate behavior across browsers). -
Focus on open: move focus to the first menu item or keep focus on the button and use
aria-activedescendantpatterns if you want roving focus. Don’t leave focus floating onbody. -
Tab behavior: for a simple action menu, I prefer not trapping focus. Let
Tabmove through items, then out. Trapping is for modals. -
Arrow keys: if you claim it’s a “menu” (with ARIA menu roles), you need roving focus with arrows. If you don’t want that complexity, don’t use
role="menu". Use a plain list of buttons/links.
ARIA: do the boring bits
-
aria-expandedon the invoker, updated when the popover opens/closes. -
aria-controlspointing at the popover id.
ARIA: avoid the tempting bits
- Avoid
role="menu"unless you fully implement menu behavior (arrow keys, Home/End, typeahead, proper focus management). Half-implementingrole="menu"is worse than not using it.
A concrete production rule: if your menu is just a list of links and buttons, treat it like that. Screen readers already know how to handle links and buttons. You don’t need to cosplay desktop app menus.
[Image: A “More actions” button with an anchored menu, showing focus outline on the first item]
Example 2: Tooltip popover (hover/focus, not an accessibility landmine)
Use case: help icon, truncated text hint, inline validation explanation.
This is where teams mess up the most because tooltips are visual sugar, and accessibility is not.
When to use a tooltip popover
- If the tooltip content is non-essential. If it’s essential, it shouldn’t be hidden behind hover.
Interaction rules
- Show on hover and keyboard focus.
- Hide on blur and mouseleave.
- Don’t steal focus. Tooltips should not take focus unless they contain interactive controls (in which case: it’s not a tooltip).
ARIA naming strategy
- If it’s purely descriptive text for a control, prefer
aria-describedbyon the control referencing the tooltip element. - Only use
role="tooltip"if the tooltip behaves like one (non-interactive, described-by relationship, appears on hover/focus, disappears on dismiss/blur).
Popover value choice
- Prefer
popover="hint"when you can, because hint popovers are designed not to close your menus. Tooltips shouldn’t collapse someone’s open menu.
Where this breaks in screen readers: if you implement tooltip content as a popover but forget to connect it via aria-describedby, VoiceOver/NVDA users won’t discover it consistently. They can still land on it in browse mode, which is confusing because it’s not really part of the reading order.
Example 3: Non-modal “mini dialog” (and when to stop pretending)
Use case: inline confirmation (“Are you sure?”), quick preferences panel, small form.
A popover can work for a small non-modal panel if:
- It doesn’t need focus trapping.
- It doesn’t need to block background interaction.
But the moment you need either of those, use <dialog>.
The difference between a popover and a dialog isn’t philosophical. It’s user expectation:
- Dialog: “Stop. Deal with this now.”
- Popover: “Here’s a contextual surface. Keep going.”
MDN’s <dialog> reference is the canonical baseline for semantics and modal behavior: MDN Contributors.
If you ship a “settings dialog” as a popover and it has form fields, you’re going to rediscover focus management bugs the hard way.
Auto versus manual popovers (how I choose in production)
This deserves its own section because teams treat it like a stylistic choice. It isn’t.
Choose auto when you want “light dismiss” and sane defaults
auto is the right call for:
- menus
- pickers
- small context panels
Because:
- outside click closes it
-
Escapecloses it - opening one closes others (reduces “popover pileups”)
That last point is huge. In big apps, you can end up with multiple open floating things. auto gives you a platform-level coordination mechanism.
Choose manual when the app owns state coordination
manual is for:
- complex flows where close should be blocked (unsaved changes)
- coordinated UI where multiple popovers must remain open together
- custom dismissal (e.g., close only on explicit action)
But manual makes you the “dismissal library.” You must:
- handle outside click
- handle
Escape - decide what happens when another popover opens
If you go manual, at least keep the logic centralized (one controller), not sprinkled across component files.
Nesting popovers (submenus without rage)
“Nesting popovers” is in the MDN popover attribute doc for a reason: it’s common, and it’s where naive dismissal logic explodes.
Typical case: a menu with a submenu.
What can go wrong:
- opening a child popover closes the parent
- moving the mouse causes accidental close when the pointer crosses a gap
- focus restoration returns to the wrong invoker
A nested strategy that doesn’t melt down:
- Parent menu is
auto. - Submenu is also
auto, but is nested in DOM under the parent popover. - When submenu opens, keep focus inside the submenu items. When it closes, restore focus to the submenu invoker item, not the top-level button.
- Only restore focus to the top-level button when the entire menu tree closes.
Use the toggle event to detect when the overall menu closes, but remember MDN’s coalescing note. Don’t write logic that assumes one event per action.
I also recommend a hard “only one submenu open at a time” rule. It’s not a desktop app. Your users will not thank you for 4 levels of hover menus.
[Image: A menu with a submenu arrow, showing flip behavior near viewport edge]
The difference between a popover and a dialog (and why is still relevant)
The web has three overlapping primitives now:
- Popover: contextual, top-layer, often light-dismiss. Not semantic.
-
<dialog>: semantic dialog container, supports modal behavior. -
<details>: disclosure widget that works everywhere, great baseline for progressive enhancement.
If you’re choosing between them, ask one question: Is background interaction allowed?
- Yes → popover or details.
- No → dialog.
And a second question: Does the content need to be in the normal document flow for reading and indexing?
- Yes → details (or just inline content).
- No → popover/dialog.
<details> remains the most underrated fallback. It’s not pretty, but it’s accessible by default, works with keyboard, and survives ancient browsers.
Progressive enhancement and fallbacks (Popover + Anchor Positioning)
This is the part most articles skip, and the part that determines whether you can actually ship.
Feature detection: don’t guess
You need to detect two independent capabilities:
- Popover API support
- Anchor Positioning support
In JS, you can check for HTMLElement.prototype.showPopover. In CSS, you can use @supports (anchor-name: --x) or similar.
A practical layered fallback strategy:
- Best: Popover + Anchor Positioning.
-
Good: Popover + basic positioning (
position: fixednear the trigger, or centered for small panels). -
Acceptable:
<details>for menus / disclosures. -
For dialog-like UI:
<dialog>.
If you absolutely must match the anchored placement and you don’t have Anchor Positioning, use a minimal JS positioning helper. Keep it small. This is where teams accidentally pull in a 20KB library to place a 180px menu.
One pattern I’ve used successfully: ship the HTML with popover and popovertarget regardless. If unsupported, your JS can “upgrade” the interaction (toggle hidden, apply a class) or swap to <details>.
Fallback for nested popovers
If you can’t rely on native dismissal coordination, nested menus become fragile fast. In fallback mode:
- avoid hover-based submenus
- switch submenus to click-to-open disclosures inside the parent
Yes, it’s less fancy. It also works.
Accessibility traps (focus management, ARIA gotchas, and screen reader reality)
This section is intentionally blunt because these are repeat offenders.
Focus management: initial focus and restore focus to invoker
When a popover opens, decide one of two models:
- Model A (simple): move focus into the popover to the first actionable element.
-
Model B (advanced): keep focus on the invoker and manage “active item” with
aria-activedescendant.
Most teams should do Model A.
On close, restore focus:
- If close was triggered by selecting an item, restore to the invoker after the action completes (or you’ll fight route changes).
- If it closed by light dismiss (outside click), restore focus to the invoker only if focus moved into the popover. If the user clicked elsewhere, don’t yank focus back like a gremlin.
Keyboard interactions a popover menu should support
My baseline for action menus:
-
Escapecloses. -
Tabmoves through items, then out. -
Shift+Tabmoves back through items. -
ArrowDown/ArrowUpoptional unless you’re using real menu roles.
If you implement arrow keys, also implement:
- Home/End
- typeahead
- wrapping behavior (decide and test)
aria-expanded / aria-controls / role=menu / role=tooltip (when helpful, when harmful)
-
aria-expanded: usually helpful on the invoker button. -
aria-controls: helpful when it references the popover id. -
role="tooltip": only for true tooltips, and pair witharia-describedby. -
role="menu": harmful unless you implement full menu interaction. If you don’t, screen readers may switch to an interaction mode that makes navigation worse.
The trap: people add roles to “make it accessible.” In reality, roles are contracts. If you don’t meet the contract, you made it less accessible.
Screen reader gotchas: naming and announcement
Popovers aren’t semantic by default. If your popover contains important content, it needs:
- a name (e.g., heading inside the panel, or
aria-labelon the container) - a relationship to the invoker (
aria-controls, and sometimesaria-haspopupdepending on UI)
Also: don’t assume “it shows visually, so it’s announced.” Screen reader users don’t see your drop shadow.
Testing checklist (keyboard, SR, zoom/reflow, forced-colors, reduced motion)
I’m going to treat this like a release checklist. If you can’t check these boxes, don’t ship.
- Keyboard-only: Can you open the popover with Enter/Space? Does focus go somewhere sane? Can you close with Escape? Can you reach every item?
-
Screen readers: Test at least 2: VoiceOver (macOS) and NVDA (Windows). Does the invoker announce expanded/collapsed? Is tooltip text reachable via
aria-describedby? - Zoom: Test at 200% and 400% zoom. Do anchored popovers flip or stay on-screen? Can you scroll to reach content?
- Reflow: Narrow the viewport to 320px CSS width. Does the menu overflow? Do you have max width and wrapping?
-
Reduced motion: With
prefers-reduced-motion: reduce, are animations removed or simplified? -
Forced colors: With
forced-colors: active, can you still see borders and focus rings? Don’t rely on box-shadow alone.
This testing list is boring. That’s why it works.
Here’s a quick demo walkthrough if you want a visual primer before implementing:
[YOUTUBE:sl_cGD327BY|How to use popover API]
Closing thought: the platform is taking your JS away. Don’t waste the win.
Popover + Anchor Positioning are the web platform quietly admitting: yes, we made you do too much for basic UI layering. In 2026, you can delete a pile of positioning code and stop importing a floating UI library by default.
But the responsibility didn’t disappear. It shifted.
If you ship popovers without an explicit keyboard and focus model, you’ll still get the same bug reports. They’ll just be harder to debug because “it’s native.” My prediction: in the next year, design systems will start treating popover focus and dismissal rules as seriously as typography tokens.
So here’s the challenge: pick one popover in your app this week. Replace the custom JS positioning with Anchor Positioning. Then write the focus rules down in the component README like it’s an API contract. If you can’t explain it, you don’t own it.
Internal reading that pairs well with this post:
- native browser APIs
- JavaScript bloat
- Tailwind CSS vs CSS Modules
- Next.js App Router vs Pages Router
- TypeScript vs JavaScript
- GitHub Actions vs CircleCI
- AI in production
- AI agents
Originally published on kunalganglani.com
Top comments (0)