CSS is Awesome

← All posts

discovery

CSS anchor positioning is a dropdown engine

anchor-name, three inset lines, and position-try-fallbacks replace the positioning half of a dropdown library — trigger-width menus that flip above the trigger when the viewport would clip them, re-evaluated by the browser on every open.

Jerry Hansen · · 4 min

  • css
  • anchor-positioning
  • popover
  • layout
  • progressive-enhancement

Every dropdown library you have ever shipped exists to answer three questions: where is the trigger, how wide is it, and is there room below it? For years the only honest answers came from JavaScript — getBoundingClientRect(), a resize observer, a scroll listener, and a positioning engine like Floating UI to glue them together.

CSS anchor positioning answers all three in the stylesheet. Not "sort of, with caveats" — the full dropdown contract, declaratively.

The wiring

Two properties connect the elements. The trigger declares a name; the menu subscribes to it:

.trigger {
  anchor-name: --menu-trigger;
}

.menu[popover] {
  position-anchor: --menu-trigger;
}

The menu here is a [popover], which matters: popovers render in the top layer, outside every overflow: hidden ancestor that has ever eaten a dropdown. Anchor positioning works on any absolutely-positioned element, but the popover pairing is the one that finally kills the portal.

Exactly the trigger's width

This is the line that made me sit up. anchor() resolves to an edge of the anchor element, and you can set both inline edges to it:

.menu[popover] {
  position-anchor: --menu-trigger;
  inset-inline: anchor(start) anchor(end);
  width: auto;
}

Left edge pinned to the trigger's left, right edge pinned to the trigger's right. The menu is now exactly as wide as its trigger — not measured once and cached, but tracked. Resize the window, let the trigger reflow, reopen the menu: it matches. No observer, no measurement, no stale width.

(The width: auto is load-bearing and non-obvious — the browser's own popover stylesheet fights you here. That line has its own story, which is the next post.)

One line below, and the flip

Placing the menu under the trigger is one more anchor():

.menu[popover] {
  inset-block-start: calc(anchor(end) + 1px);
  position-try-fallbacks: flip-block;
}

anchor(end) on the block axis is the trigger's bottom edge; the calc() opens the menu one pixel below it. And position-try-fallbacks: flip-block is the part that used to be a library's whole reason to exist: if the menu would overflow the bottom of the viewport, the browser flips it to sit above the trigger instead. This is not a one-time decision — the browser re-evaluates on every open, at the element's current scroll position. Trigger near the bottom of the page: menu opens upward. Scroll it back up, open again: downward. Zero lines of script.

Progressive enhancement, honestly

Anchor positioning is not Baseline yet, so this cannot be the only path. The shape that works is a fallback-first rule with the anchored version layered inside @supports:

.menu[popover] {
  /* no-anchor fallback: full viewport width minus the page gutters */
  inset-inline: var(--space-4);
  width: auto;

  @supports (anchor-name: --a) {
    position-anchor: --menu-trigger;
    inset-inline: anchor(start) anchor(end);
    inset-block-start: calc(anchor(end) + 1px);
    position-try-fallbacks: flip-block;
  }
}

Browsers without anchor support get a menu spanning the viewport inside the gutters — on a phone that is close to what you wanted anyway. Browsers with it get the trigger-width, flip-aware version.

Where support stands as I write this: Chrome and Edge since 125, Firefox since 144, Safari in Technology Preview only. That is most of the market but not all of it, and "Safari TP" means real iOS users see your fallback today. Ship the @supports block or don't ship this at all.

The takeaway

The interesting shift is not that this saves bytes. It is that the dropdown's positioning brain moved into the browser, where it belongs — the same engine that already knows about scroll, zoom, writing modes, and the keyboard insetting the viewport. A JS positioning library is a reimplementation of layout knowledge the browser had all along; anchor() is just being allowed to ask for it.


We use this in css-is-awesome: cia.dropdown ships exactly this pattern — fallback-first, anchored under @supports — and it's rule #8 in the package's AGENTS.md. You can see it flip in the dashboard-shell demo at /docs/recipes.

← All posts

Theme