A dropdown menu sounds like a small feature.
Then the edge cases arrive.
It needs to appear above the rest of the page, close when the user clicks elsewhere, respond to the Escape key, stay attached to its trigger while the page moves, and avoid disappearing beyond the viewport.
For years, the usual answer was a collection of JavaScript event listeners, layout measurements, portals, z-index rules, and sometimes a positioning library.
The web platform now has a more direct answer.
The Popover API handles showing, hiding, light dismissal, and placement in the browser's top layer. CSS Anchor Positioning connects the floating element to its trigger and lets the browser handle the layout relationship.
Together, they cover a surprisingly large set of everyday interface patterns.
Table of contents
- The problem these APIs solve
- Build the smallest native popover
- Attach it to the trigger
- Keep it inside the viewport
- Choose the right popover mode
- Build a practical action menu
- Accessibility still matters
- When JavaScript is still the right tool
- A migration checklist
The problem these APIs solve
Floating interfaces are common:
- account menus,
- share panels,
- filter controls,
- formatting toolbars,
- teaching tips,
- color pickers,
- compact action menus.
They all have two separate problems.
First, the interface needs behavior. It must open, close, respond to expected controls, and appear above ordinary page content.
Second, it needs positioning. It must stay connected to the element that opened it and adjust when the available space changes.
The Popover API addresses the first problem. CSS Anchor Positioning addresses the second.
Keeping those responsibilities separate makes the feature easier to understand:
Popover API β visibility and top-layer behavior
CSS Anchor Positioning β relationship to the trigger
Your application β business logic and content
The APIs work independently, but they are particularly useful together.
According to MDN, the Popover API reached Baseline 2025 and provides a standard mechanism for non-modal content displayed above other page content. Typical uses include action menus, form suggestions, content pickers, notifications, and teaching UI.
CSS Anchor Positioning is the newer half of the combination. It allows a positioned element to reference another element as its anchor. Current browser documentation lists support in modern Chrome, Edge, Firefox, and Safari releases, so this is now a feature worth evaluating for production rather than filing away as an experiment.
Popover is not dialog
A popover is non-modal. The rest of the page remains available while it is open.
Use a popover for contextual controls or supplementary content. Use <dialog> when the user must deal with a modal decision before returning to the page.
A confirmation prompt for deleting an account is a dialog. A compact menu containing account actions is a popover.
That distinction affects semantics, focus expectations, and the user's ability to continue interacting with the document.
Build the smallest native popover
The basic version needs a button and a target element:
<button type="button" popovertarget="profile-actions">
Profile actions
</button>
<div id="profile-actions" popover>
<p>Manage your profile and preferences.</p>
</div>
No JavaScript is required to toggle it.
The popovertarget value points to the id of the popover. A button toggles its target by default. When the popover opens, the browser places it in the top layer, above ordinary stacking contexts.
An automatic popover also receives light-dismiss behavior. The user can close it by clicking elsewhere or pressing Escape.
That replaces a familiar block of manual work:
// The kind of plumbing often added before native popovers
trigger.addEventListener("click", toggleMenu);
document.addEventListener("click", closeWhenOutside);
document.addEventListener("keydown", closeOnEscape);
The native version is not merely shorter. It gives the browser a declared relationship between the control and the overlay.
Use explicit actions when the interface needs them
A button can be limited to showing or hiding the target:
<button
type="button"
popovertarget="help-panel"
popovertargetaction="show"
>
Open help
</button>
<div id="help-panel" popover>
<p>Keyboard shortcuts are available in settings.</p>
<button
type="button"
popovertarget="help-panel"
popovertargetaction="hide"
>
Close
</button>
</div>
Supported actions are show, hide, and toggle. If popovertargetaction is omitted, the button toggles the popover.
Attach the popover to its trigger
The first example opens correctly, but it does not yet describe where the popover belongs relative to the button.
Give the trigger an anchor name:
.profile-trigger {
anchor-name: --profile-trigger;
}
Then connect the popover to that anchor:
.profile-popover {
position: fixed;
position-anchor: --profile-trigger;
position-area: block-end span-inline-end;
margin: 0.5rem 0 0;
}
Complete HTML:
<button
class="profile-trigger"
type="button"
popovertarget="profile-actions"
>
Profile actions
</button>
<div
class="profile-popover"
id="profile-actions"
popover
>
<p>Manage your profile and preferences.</p>
</div>
anchor-name identifies the reference element. position-anchor selects that reference for the positioned element. position-area places the popover in an area around the anchor.
Logical terms such as block-end and inline-end are preferable to assuming that every interface reads from left to right. They follow the document's writing mode.
Position with anchor() when you need more control
The anchor() function exposes coordinates from the anchor element:
.profile-popover {
position: fixed;
position-anchor: --profile-trigger;
inset-block-start: anchor(bottom);
inset-inline-end: anchor(right);
margin-block-start: 0.5rem;
}
This can be useful when position-area is not precise enough. For common menus and teaching tips, however, position-area often expresses the intention more clearly.
Keep it inside the viewport
Anchoring a menu below a button works until the button sits near the bottom of the viewport.
CSS provides fallback positioning for that case:
.profile-popover {
position: fixed;
position-anchor: --profile-trigger;
position-area: block-end span-inline-end;
position-try-fallbacks: flip-block, flip-inline;
margin: 0.5rem;
}
The browser can try another placement when the preferred position does not fit. flip-block moves the overlay to the opposite side on the block axis. flip-inline provides a corresponding fallback on the inline axis.
For a custom sequence, define named alternatives:
@position-try --above-trigger {
position-area: block-start span-inline-end;
}
@position-try --start-of-trigger {
position-area: span-block-start inline-start;
}
.profile-popover {
position: fixed;
position-anchor: --profile-trigger;
position-area: block-end span-inline-end;
position-try-fallbacks:
--above-trigger,
--start-of-trigger;
}
This is the part that traditionally required viewport measurements and resize or scroll handling. The browser now has the information needed to choose from declared alternatives.
Do not create a dozen fallback positions just because the API allows it. Most compact overlays need a preferred position and one or two sensible alternatives.
Choose the right popover mode
The popover attribute supports different modes. The choice changes how the element behaves alongside other popovers.
Automatic popovers
<div popover="auto">...</div>
Writing only popover is equivalent to popover="auto".
Automatic popovers support light dismissal. Opening another automatic popover generally closes the previous unrelated one, while nested relationships can remain open together.
This is a good default for menus and contextual controls.
Manual popovers
<div popover="manual">...</div>
Manual popovers do not get automatic light dismissal and do not close merely because another popover opens. Your code or an explicit control must manage their state.
That can be appropriate for notifications or interfaces that should remain visible until the application decides otherwise.
Hint popovers
<div popover="hint">...</div>
Hint popovers are intended for short-lived contextual content, such as hover or focus hints, and have different stacking interactions from ordinary automatic popovers. Because this area is newer, check the support requirements of your audience before making it essential to the experience.
Quick selection guide
auto for action menus, pickers, and contextual panels.manual when application logic owns the complete lifecycle.hint for short-lived hints where your browser support policy allows it.<dialog> rather than a popover for a truly modal decision.
Build a practical action menu
Here is a complete small component with no JavaScript required for opening, closing, or positioning.
<div class="article-actions">
<button
class="action-trigger"
type="button"
popovertarget="article-menu"
aria-label="Open article actions"
>
Actions
</button>
<div
class="action-menu"
id="article-menu"
popover
>
<button type="button">Save for later</button>
<button type="button">Copy link</button>
<button type="button">Share</button>
</div>
</div>
.action-trigger {
anchor-name: --article-actions;
}
.action-menu {
position: fixed;
position-anchor: --article-actions;
position-area: block-end span-inline-end;
position-try-fallbacks: flip-block, flip-inline;
inline-size: max-content;
min-inline-size: 12rem;
margin: 0.5rem;
padding: 0.4rem;
border: 1px solid color-mix(in srgb, CanvasText 18%, transparent);
border-radius: 0.75rem;
background: Canvas;
color: CanvasText;
box-shadow: 0 0.8rem 2rem rgb(0 0 0 / 0.16);
}
.action-menu button {
display: block;
inline-size: 100%;
padding: 0.65rem 0.8rem;
border: 0;
border-radius: 0.5rem;
background: transparent;
color: inherit;
font: inherit;
text-align: start;
cursor: pointer;
}
.action-menu button:hover,
.action-menu button:focus-visible {
background: color-mix(in srgb, CanvasText 9%, transparent);
}
The interface still needs application code for actions such as copying a link or saving an item. Native APIs remove the generic overlay plumbing, not the feature's real behavior.
For example:
document
.querySelector("[data-copy-link]")
?.addEventListener("click", async () => {
await navigator.clipboard.writeText(location.href);
});
That is a healthier division of responsibility. JavaScript handles the action that only JavaScript can perform. HTML and CSS handle the generic interface mechanics.
Style the open state
A popover can be targeted with :popover-open:
.action-menu:popover-open {
opacity: 1;
transform: translateY(0);
}
Transitions require care because opening and closing a popover changes properties such as display and moves the element into or out of the top layer. Modern CSS includes tools for discrete transitions, but animation should remain an enhancement rather than a condition for understanding the interface.
Always respect reduced-motion preferences:
@media (prefers-reduced-motion: no-preference) {
.action-menu {
transition:
opacity 150ms ease,
transform 150ms ease,
display 150ms allow-discrete,
overlay 150ms allow-discrete;
}
.action-menu:not(:popover-open) {
opacity: 0;
transform: translateY(-0.25rem);
}
}
Test both opening and closing in the exact browsers your project supports. Overlay animation support has evolved separately from the basic Popover API.
Accessibility still matters
Native behavior removes some common mistakes, but it does not guarantee that the finished component is accessible.
Use a real button as the invoker
Do not attach the interaction to a generic <div>. A button is keyboard accessible and communicates that it performs an action.
<button type="button" popovertarget="settings-menu">
Settings
</button>
Give the control a clear accessible name
An icon-only trigger needs an accessible label:
<button
type="button"
popovertarget="settings-menu"
aria-label="Open settings menu"
>
<!-- decorative icon -->
</button>
Match semantics to the content
The word βmenuβ has a specific meaning in accessibility APIs. Do not add role="menu" merely because a panel looks like a menu visually. A group of ordinary links or buttons may not need application-menu semantics.
If you implement a true ARIA menu, you also take responsibility for its keyboard interaction model. Native popover behavior does not implement that complete pattern for you.
Test focus, not only clicks
Verify the component with:
- keyboard navigation,
- visible focus indicators,
- Escape dismissal,
- browser zoom,
- screen-reader output,
- high-contrast or forced-color modes,
- reduced-motion preferences,
- right-to-left content if your product supports it.
Keep essential information available
A tooltip should not be the only place where critical instructions or validation errors appear. Floating supplemental content can be missed by touch users, keyboard users, and assistive technology users when the trigger or interaction is poorly chosen.
When JavaScript is still the right tool
Native popovers and anchor positioning remove a lot of infrastructure, but they do not make positioning libraries or component frameworks obsolete.
Keep JavaScript when you need:
- asynchronous content loading tied to application state,
- complex focus movement and composite widgets,
- virtual anchors that do not exist as DOM elements,
- advanced collision strategies beyond declared CSS fallbacks,
- analytics or lifecycle coordination,
- controlled state shared with a framework,
- compatibility with older browsers in your actual support matrix,
- highly specialized interactions such as rich autocomplete or nested application menus.
The goal is not βzero JavaScript.β The goal is less custom JavaScript for behavior the browser already understands.
A mature component may use all three layers:
HTML β declares the trigger and popover relationship
CSS β anchors and styles the overlay
JS β loads data, performs actions, and coordinates state
That is still a native-first implementation.
Use progressive enhancement deliberately
Before removing an existing library, check your browser analytics and support policy.
Feature detection can protect a nonessential enhancement:
@supports (anchor-name: --trigger) {
.action-trigger {
anchor-name: --trigger;
}
.action-menu {
position-anchor: --trigger;
position-area: block-end span-inline-end;
}
}
For an existing product, migration does not need to happen everywhere at once. Start with a low-risk component, measure behavior, and keep the previous implementation where requirements exceed native support.
Do not replace a well-tested system only to remove a dependency. Replace it when the native implementation is simpler for your use case and satisfies your accessibility, compatibility, and maintenance requirements.
A practical migration checklist
Choose the candidate
- [ ] The component is non-modal.
- [ ] It opens from a real DOM element.
- [ ] Its positioning can be described relative to that element.
- [ ] It needs ordinary opening, dismissal, and viewport fallback behavior.
- [ ] It is not dependent on a highly specialized widget interaction model.
Build the native version
- [ ] Use
popoveron the floating element. - [ ] Connect a button with
popovertarget. - [ ] Add
anchor-nameto the trigger. - [ ] Add
position-anchorandposition-areato the popover. - [ ] Declare one or two sensible fallback positions.
- [ ] Keep business logic in JavaScript rather than rebuilding overlay plumbing.
Verify the experience
- [ ] Test mouse, touch, and keyboard interaction.
- [ ] Verify Escape and light dismissal.
- [ ] Check viewport edges at several zoom levels.
- [ ] Test long labels and dynamic content.
- [ ] Confirm logical positioning in supported writing modes.
- [ ] Test with a screen reader and visible focus.
- [ ] Check the browsers and versions in your real support matrix.
Compare with the existing implementation
- [ ] Is the native version easier to understand?
- [ ] Does it remove meaningful event and measurement code?
- [ ] Are any capabilities lost?
- [ ] Is the fallback behavior acceptable?
- [ ] Can the dependency be removed completely, or is it still needed elsewhere?
The takeaway
The web platform is absorbing another category of work that used to belong almost entirely to JavaScript.
The Popover API gives browsers a native way to show non-modal content in the top layer, connect it to an invoker, and provide common dismissal behavior. CSS Anchor Positioning lets the overlay stay attached to its trigger and try alternative placements when space is limited.
The result is not a ban on JavaScript or positioning libraries.
It is a better default question:
Can the browser handle the generic interaction while my code handles the feature itself?
For many action menus, teaching tips, pickers, and contextual panels, the answer is now yes.
Which small overlay in your current project would be the safest candidate for a native rewrite?
Explore the Popover API on MDN
Sources and further reading
- MDN: Popover API
- MDN: Using the Popover API
- Chrome for Developers: The CSS Anchor Positioning API
Connect with Me
If you found this article helpful, let's connect!
- π» GitHub: johnnylemonny
Top comments (0)