DEV Community

Cover image for Stop Using JavaScript for Every Popover: Modern HTML and CSS Can Do More Than You Think
𝗝𝗼𝗡𝗻
𝗝𝗼𝗡𝗻

Posted on AI-assisted

Stop Using JavaScript for Every Popover: Modern HTML and CSS Can Do More Than You Think

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.


The useful shift: start with native HTML and CSS for ordinary popovers, menus, and anchored overlays. Add JavaScript only when the interaction genuinely needs application logic.

Table of contents


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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.


Prefer the most declarative rule that describes the layout. Reach for coordinate-level control only when the design actually requires it.

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
  • Use auto for action menus, pickers, and contextual panels.
  • Use manual when application logic owns the complete lifecycle.
  • Evaluate hint for short-lived hints where your browser support policy allows it.
  • Use <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>
Enter fullscreen mode Exit fullscreen mode
.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);
}
Enter fullscreen mode Exit fullscreen mode

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);
  });
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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 popover on the floating element.
  • [ ] Connect a button with popovertarget.
  • [ ] Add anchor-name to the trigger.
  • [ ] Add position-anchor and position-area to 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


Connect with Me

If you found this article helpful, let's connect!

Top comments (0)