RSSAmplifier

Karl Koch · Jul 9, 2026

On anchored interfaces

0
Sign in to vote or save

Karl Koch · Karl Koch

Floating UI is one of those problems that looks small until you build it properly.

You click a button and a menu appears below it, which sounds like the whole component until the page scrolls. The trigger moves, the menu clips against the viewport, a parent has overflow: hidden, or a modal has a higher z-index. Escape and clicking outside should close it; focus should move somewhere sensible, then return when it closes. The menu should not render halfway off-screen just because the trigger is near the bottom edge.

“Put this panel under that button” soon requires geometry, focus management, dismissal logic, portals, resize observers, and another fight with z-index: 9999.

The platform now handles much of that work.

The old shape

The JavaScript version usually has three jobs.

First, measure the trigger:

const rect = trigger.getBoundingClientRect();

Then place the panel:

panel.style.top = `${rect.bottom + 8}px`;
panel.style.left = `${rect.left}px`;

Then keep it correct:

window.addEventListener("resize", updatePosition);
window.addEventListener("scroll", updatePosition, true);
document.addEventListener("pointerdown", closeOnOutsideClick);
document.addEventListener("keydown", closeOnEscape);

This is not wrong. Libraries like Floating UI exist because this problem has real edge cases and teams need reliable answers.

But for a lot of menus, small popovers, action panels, and teaching UI, the browser can own more of the contract now.

Popover owns the layer

The Popover API starts with plain HTML:

<button popovertarget="actions-menu">Actions</button>
<div id="actions-menu" popover>
  <button type="button">Rename</button>
  <button type="button">Duplicate</button>
  <button type="button">Archive</button>
</div>

That popover attribute handles several behaviours.

The panel is hidden by default until the trigger opens it, while Escape or a click outside closes it. The browser promotes the panel to the top layer, freeing it from parent stacking contexts, and handles focus behaviour instead of relying on a handwritten click-outside hook that misses half the cases.

The design work can move back to the relationship between trigger and surface: size, spacing, alignment, density, affordance.

Anchor positioning owns the tether

The missing piece is geometry. A top-layer element is useful because it escapes clipping, but it still needs to appear attached to the thing that opened it.

CSS Anchor Positioning gives the browser that relationship:

[popover] {
  inset: auto;
  margin: 0;
  position-area: bottom span-right;
}

When a popover is opened by a popovertarget, the browser creates an implicit anchor between the trigger and the panel. For simple cases, position-area is enough. “Put this below the trigger and align it to the right” becomes a CSS declaration, not a measurement loop.

For more exact placement, anchor() lets you bind edges directly:

.menu {
  inset: auto;
  top: calc(anchor(bottom) + 0.5rem);
  right: anchor(right);
  min-width: anchor-size(width);
}

Layout now keeps the menu tied to the trigger’s geometry without JavaScript copying its rectangle into inline styles.

The edge test

The moment an overlay reaches a viewport edge, taste and engineering meet.

A menu that clips off-screen feels broken, while one that jumps late feels cheap. Overlapping its trigger in a cramped space might be correct if the alternative is worse. These are design decisions, but they need layout primitives to behave consistently.

With anchor positioning, the fallback can live in CSS:

.menu {
  position-area: bottom span-right;
  position-try-fallbacks: flip-block;
}

If the preferred placement does not fit below the trigger, the browser can try the block-axis opposite. The component keeps its spatial relationship without an animation frame loop watching for collisions.

For more specific fallbacks:

@position-try --menu-above {
  position-area: top span-right;
}
.menu {
  position-area: bottom span-right;
  position-try-fallbacks: --menu-above;
}

The design intent stays declarative: prefer below, align right, and flip above when space runs out.

Drag the trigger

A button sits inside a small viewport. Drag it near each edge. The panel follows it, flips when needed, and keeps the same visual tether.

The demo shows how much positioning code the browser primitive removes.

Popover is not semantics

There is one important catch: popover gives you behaviour, not meaning.

This:

<div popover>Settings</div>

does not become a menu, tooltip, or dialog. It is a top-layer popover, and you still need the right semantics for its content.

A menu of actions should contain buttons. A tooltip needs a relationship to the thing it describes. A modal task belongs in dialog, not a non-modal popover. A disclosure might not need popover at all.

That distinction matters. The platform can handle the layer and the tether. It does not decide what you meant.

Styling the open state

Popover also gives you a clean styling hook:

[popover] {
  opacity: 0;
  transform: translateY(-0.25rem) scale(0.98);
}
[popover]:popover-open {
  opacity: 1;
  transform: translateY(0) scale(1);
}

For entry and exit transitions, discrete properties like display need care, but the basic open-state selector is enough for many UI details: shadow depth, opacity, border colour, icon rotation on the trigger, backdrop treatment, and so on.

These visible details shape the component after the browser handles its positioning.

When not to use it

Popover and anchor positioning are not a universal replacement for every floating surface.

I would still be cautious with:

  • complex nested menus
  • virtualised lists
  • text selection toolbars with unusual input constraints
  • comboboxes that need deep autocomplete semantics
  • browser support requirements that cannot tolerate progressive enhancement
  • heavily choreographed surfaces where the animation system owns layout

Overlay libraries still suit complex cases. Check whether the platform covers the ordinary case before adding one.

When to steal this

Use anchored popovers for action menus, small pickers, contextual panels, teaching UI, and non-modal surfaces that should stay visually attached to a trigger.

Start with popover for layer and dismissal. Add position-area for simple placement. Reach for anchor() when exact edges matter. Add position-try-fallbacks when the trigger can approach viewport edges.

A good anchored popover feels attached to its trigger, behaves predictably, and leaves little positioning code to maintain.

Read the original on karlkoch.me

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.