The Glitched Goblet Logo

The Glitched Goblet

Where Magic Meets Technology

Native Popovers: Let CSS Handle the Awkward Positioning

October 12, 2026

Intro

A button opens a little panel. Simple enough.

Until the panel gets clipped by its parent. So you move it somewhere else in the DOM, which means it needs coordinates. Then the user scrolls. Then they open it near the bottom of a phone screen. Before long, your tiny panel has accumulated a pile of JavaScript just to figure out where it should exist.

These days, the browser can handle a lot of that work for us.

This post uses the Popover API and CSS anchor positioning to build a small floating panel without manually managing its position in JavaScript.

tldr; The Popover API handles opening, closing, and displaying a panel above the page. CSS anchor positioning places it beside its trigger and can try alternate positions when space gets tight. Keep a fallback for older browsers and test the finished thing with a keyboard.

Why revisit this now?

Anchor positioning and improvements to dialogs and popovers are part of Interop 2026, the cross-browser effort to make web features behave more consistently.

That matters because these APIs have existed in various stages for a while, but browser support is finally reaching the point where they're worth considering for real projects.

As of this post, MDN lists position-area as Baseline Newly available, with support across the latest browsers since January 2026.

Older devices haven't magically disappeared, though, so we'll build this progressively instead of assuming everyone is running the latest browser.

Baseline is useful for checking browser availability, but it isn't a replacement for knowing your audience. It also doesn't tell you whether your particular implementation is accessible. I wouldn't delete an existing dependency just because the Baseline badge turned green.

Start with a button and some links

We'll build a small panel for finding more Glitched Goblet posts. You can paste the HTML into a page and put the CSS in a <style> element.

<button class="browse-trigger" type="button" popovertarget="browse-posts">
  Explore the Goblet
</button>

<nav
  id="browse-posts"
  class="browse-panel"
  popover="auto"
  aria-label="Explore the Goblet"
>
  <a href="https://glitchedgoblet.blog/post/dev/css-fun">Fun CSS effects</a>
  <a href="https://glitchedgoblet.blog/post/dev/js-proxy">JavaScript Proxy</a>
  <a href="https://glitchedgoblet.blog/magic/my-edh-approach">My EDH approach</a>
</nav>

The button's popovertarget matches the panel's id. In a supporting browser, the panel starts hidden and the button toggles it.

With popover="auto", clicking outside the panel or pressing Escape closes it. While open, the panel is placed in the browser's top layer, which means an ancestor with overflow: hidden won't slice it in half. (MDN: popover attribute)

That also means there's no click-outside listener for us to wire up and maintain.

A popover is non-modal, so the rest of the page stays interactive. If the interaction actually needs to block the rest of the page, <dialog> with showModal() is probably the better tool. (MDN: Popover API)

Give it some personality

Here's the basic styling:

.browse-trigger {
  display: none;
  padding: 0.75rem 1rem;
  border: 1px solid #00e9ff;
  border-radius: 0.5rem;
  background: #151522;
  color: #fafafa;
  font: inherit;
  cursor: pointer;
}

.browse-panel {
  box-sizing: border-box;
  width: 18rem;
  max-width: calc(100vw - 2rem);
  padding: 0.75rem;
  border: 1px solid #00e9ff;
  border-radius: 0.75rem;
  background: #151522;
  color: #fafafa;
  overflow-wrap: anywhere;
}

.browse-panel a {
  display: block;
  padding: 0.75rem;
  border-radius: 0.25rem;
  color: inherit;
}

.browse-panel a:hover {
  background: #30304a;
}

.browse-trigger:focus-visible,
.browse-panel a:focus-visible {
  outline: 3px solid #00e9ff;
  outline-offset: 3px;
}

@supports selector(:popover-open) {
  .browse-trigger {
    display: inline-block;
  }

  .browse-panel {
    position: fixed;
    inset: 0;
    margin: auto;
    max-height: calc(100dvh - 2rem);
    overflow: auto;
  }
}

The trigger starts with display: none on purpose.

A browser without popover support will ignore the popover and popovertarget attributes, which leaves the links sitting in the normal document flow. There's no reason to show those browsers a button that can't do anything.

Browsers that recognize :popover-open get the trigger and a centered popover. We haven't added anchor positioning yet, but this gives us a usable fallback before we do anything fancy.

And yes, it gets a little neon border. I'm not completely abandoning my principles here.

There is one annoying detail worth knowing about: don't put display: grid or display: flex directly on .browse-panel.

Doing that can override the browser's closed-state display: none. If you need a layout mode inside the popover, style an inner wrapper or apply it specifically through .browse-panel:popover-open. (MDN: Using the Popover API)

Attach it to the button

Now add the anchor positioning rules after the previous CSS:

@supports (position-area: block-end span-inline-end) and
  (position-try-fallbacks: flip-block) {
  .browse-panel {
    inset: auto;
    margin: 0.5rem;
    position-area: block-end span-inline-end;
    position-try-fallbacks:
      flip-block,
      flip-inline,
      flip-block flip-inline;
  }
}

You might notice there isn't an anchor-name anywhere.

In this case, popovertarget already establishes an implicit anchor relationship between the button and its popover, so the CSS can use that relationship directly.

For elements that don't already have that relationship, you can create one explicitly with anchor-name: --my-button on the anchor and position-anchor: --my-button on the positioned element. (MDN: Using CSS anchor positioning)

position-area chooses which region around the anchor should contain the panel.

With normal horizontal, left-to-right text, block-end span-inline-end places the panel below the button and allows it to extend toward the right. Because these are logical directions rather than physical top, right, bottom, and left values, they also adapt to the writing direction.

We reset inset and margin here because the earlier fallback centered the popover with inset: 0 and margin: auto. Those rules would interfere with the anchored placement.

The 0.5rem margin leaves a little space between the button and the panel. (MDN: position-area)

What happens at the edge?

The initial position won't always fit. That's where position-try-fallbacks comes in.

We've given the browser three alternatives:

  1. flip-block moves the panel to the opposite side of the block axis, which usually means above the button.
  2. flip-inline lets it extend toward the opposite inline direction.
  3. flip-block flip-inline flips both directions together.

The commas are important. Each comma separates a different fallback attempt. flip-block flip-inline is a single fallback that applies both flips at once. (MDN: position-try-fallbacks)

These fallbacks can only work with the space that actually exists. If the panel is too large for every available placement, it can still overflow.

Keep floating panels reasonably small, test them on narrow screens, and consider falling back to the centered version when the viewport gets cramped. Large text and small screens can burn through your available space pretty quickly.

Accessibility notes

The example uses a <nav> containing ordinary links.

I wouldn't add role="menu" here unless I actually intended to build menu behavior. That role comes with expectations around things like arrow-key navigation and focus management, and native popovers don't implement all of that for you.

For a simple navigation panel like this, regular links with normal Tab navigation are a better fit. (MDN: menu role)

The native relationship between the trigger and popover also helps with keyboard focus, including returning focus when the popover is dismissed. That doesn't mean you get to skip testing it. (MDN: Using the Popover API)

Before shipping it, I'd check at least these cases:

  • Tab to the trigger and open it with Enter or Space.
  • Tab through the links, press Escape, and make sure focus ends up somewhere sensible.
  • Open it near every edge of the viewport.
  • Try it inside a scrolling container.
  • Zoom to 200% and use longer link labels.
  • Check the navigation's name and links with a screen reader.
  • Disable the anchor positioning rule and make sure the centered fallback still works.
  • Test a browser without popover support and confirm the inline links are still usable.

Feature detection can tell you whether the browser understands the CSS. It can't tell you whether the thing you built is comfortable to use.

Try it somewhere small

I wouldn't start by ripping out every dropdown library in an application.

Pick one simple interaction first: a related-links panel, a help disclosure, or something else that doesn't need complicated focus management or nested behavior.

If your existing component library is handling things like nested menus, advanced keyboard interaction, or older browser support, there's still a good reason to keep it around.

For more detail, the MDN anchor positioning guide is worth having open while experimenting.

The Interop 2026 overview is also worth a look if you want to see what other browser features are becoming more practical this year.

Outro

I like browser features that let me delete code I never wanted to maintain in the first place.

For a small floating panel, I'd much rather give the browser a trigger, a preferred position, and a few fallback options than keep recalculating coordinates every time somebody scrolls or resizes the page.

Give it a real button, normal links, a fallback for older browsers, and then let CSS deal with the awkward geometry.

The floating goblins can finally have proper anchors.

Plugs

Enjoy my content? You can read more at The Glitched Goblet or follow me on BlueSky at kaemonisland.bsky.social.

I write about web development and Magic: The Gathering. Sometimes the stack behaves in either one.