Anchored UI Without a Positioning Library
How the Popover API and CSS anchor positioning replaced JavaScript positioning libraries: tethering, position-area, edge fallbacks, and the support floor.
Every design system eventually grows the same three components: a dropdown, a tooltip, and a popover. Every one of them needs the same thing — an element pinned to another element, flipped when it runs out of room, and kept alive across scroll and resize. For a decade the answer was a JavaScript positioning library plus a portal. In 2026 most of that work is a CSS and HTML problem instead.
What the browser now does for you
The CSS anchor positioning module defines features that allow you to tether elements together, where the size and position of an anchor-positioned element are set based on the size and location of the anchor it is bound to.[1] The module also covers the part that actually made libraries necessary: it provides CSS-only mechanisms to specify alternative positions for an anchored element when the default rendering position causes it to overflow its containing block or be rendered offscreen, and to declare conditions under which the element should be hidden.[1]
That is the real argument. Historically, associating an element with another element and dynamically changing a positioned element's location and size based on an anchor's position required JavaScript, which added complexity and performance issues and was not guaranteed to work in all situations.[2] The failure mode engineers hit is not the arithmetic — it is that the trigger sits inside an overflow: hidden container or a stacking context that traps z-index, so the panel gets clipped and the team reaches for a portal.
The properties that matter
Associating the two elements takes three declarations. The anchor declares a name; the positioned element must be absolutely or fixed positioned, then points at that name.[2]
.trigger {
anchor-name: --account-menu;
}
.menu {
position: absolute;
position-anchor: --account-menu;
}Association alone does not move anything — MDN is explicit that the two elements are associated but not yet tethered.[2] Placement is the second step, and there are two styles. The anchor() function gives coordinate-level control inside inset properties, returning a length relative to the position of the edges of its associated anchor; top: anchor(bottom) reads the anchor's bottom edge.[13] The position-area property is the higher-level option: it places the positioned element on one or more tiles of an implicit 3x3 grid where the anchoring element is the center cell.[3]
position-area is the one to reach for first, because the grid model expresses the common cases in one line and reads like the intent:
.menu {
position: absolute;
position-anchor: --account-menu;
position-area: block-end span-inline-start;
margin-block-start: 0.5rem;
min-width: anchor-size(width);
}That last line is worth pausing on. anchor-size() sizes or positions an anchor-positioned element based on the size of its anchor, so a menu can match its trigger's width without a ResizeObserver.[8]
Overflow is the whole argument
Flipping is what you were paying the library for. position-try-fallbacks takes an ordered list of alternative placements; when the element would otherwise overflow its inset-modified containing block, the browser tries placing it in those fallback positions, in the order provided, until it finds a value that stops it from overflowing its container or the viewport.[4] The built-in tactics mirror the current placement — flip-block flips the element's position along the block axis, flip-inline along the inline axis — and for anything more than a mirror you define a named option with @position-try.[4]
@position-try --menu-above {
position-area: block-start span-inline-start;
margin-block-start: 0;
margin-block-end: 0.5rem;
max-height: 40vh;
overflow-y: auto;
}
.menu {
position-try-fallbacks: --menu-above, flip-block, flip-inline;
}A custom position try option is named with a <dashed-ident> and contains a descriptor list specifying inset position, margin, sizing, and self-alignment; the browser selects the first option it finds that places the positioned element fully back on-screen.[5] Because that evaluation happens during layout rather than in a scroll handler, it stays correct through scroll and resize without a listener. When no position is acceptable, position-visibility handles the exit condition, enabling you to conditionally hide an anchor-positioned element depending on whether it is overflowing its containing element or the viewport.[10]
Pair it with the Popover API
Anchor positioning answers *where*. The Popover API answers *whether*, and it answers accessibility questions you would otherwise write by hand. MDN has it Baseline since 2025.[6] When open, popover elements appear above all other elements in the top layer and are not influenced by parent elements' position or overflow styling.[15] The top layer is a layer that spans the entire width and height of the viewport and sits on top of all other layers displayed in the document.[16]
The default auto state does the interaction work: auto popovers can be light dismissed by clicking outside them or pressing Escape, and showing one generally closes other auto popovers that are already displayed.[15] For a menu that means promotion out of every stacking context, an outside click that closes and returns focus, and Escape handling — none of which you write.[7]
The combination is where the JavaScript disappears. When a popover is associated with its control declaratively through popovertarget and id, an implicit anchor reference is made between the two, which means you can skip anchor-name and position-anchor entirely.[2]
<button popovertarget="account-menu">Account</button>
<div id="account-menu" popover>
<a href="/settings">Settings</a>
<a href="/billing">Billing</a>
</div>Two footguns are worth knowing before you ship this. Popovers carry default margin and inset styles that may conflict with the position you are trying to achieve, so reset those first — MDN calls them the usual culprits.[3] And if you render a list of components that each use the same anchor name, the positioned element is associated with the last anchor element in the source order, not the one in its own subtree.[9] anchor-scope on the repeated container fixes that by limiting the scope in which a positioned element can be associated with anchors to that subtree.[9]
Support and the fallback you actually need
Browser-compat-data records anchor-name from Chrome 125, Firefox 147, and Safari 26; position-area from Chrome 129; position-try-fallbacks from Chrome 128; anchor-scope from Chrome 131; and position-visibility from Safari 26.2.[12] Caniuse currently reports 0.01% full and 85.91% partial global support, scoring Safari 27 as the first fully supported release.[11] Watch the property names in older material: Chrome shipped inset-area before the rename to position-area, and position-try-options before position-try-fallbacks, with the renames landing in Chrome 129.[8]
One subtlety that bites teams on the way in: position-anchor only settled on a normal initial value recently — Chrome and Firefox in 151, Safari in 27. Before that it initialised to auto or none, and the difference matters, because normal behaves as none when position-area is none and as auto otherwise.[12][14]
The graceful part is the failure mode. A popover opened without anchor positioning support still appears in the top layer, and position-area simply has no effect on an element that has no default anchor element — so the placement degrades to the default, and nothing else in the component breaks.[3][15] Scope the anchored layout inside @supports (anchor-name: --probe) and leave a plain absolutely positioned dropdown outside it. That fallback does not flip, but it works, and it costs one feature query instead of a positioning library you have to keep updating.
Sources
[1] https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning
[2] https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning/Using
[3] https://developer.mozilla.org/en-US/docs/Web/CSS/position-area
[4] https://developer.mozilla.org/en-US/docs/Web/CSS/position-try-fallbacks
[5] https://developer.mozilla.org/en-US/docs/Web/CSS/@position-try
[6] https://developer.mozilla.org/en-US/docs/Web/API/Popover_API
[7] https://web.dev/blog/popover-api
[8] https://developer.chrome.com/blog/anchor-positioning-api
[9] https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/anchor-scope
[10] https://developer.mozilla.org/en-US/docs/Web/CSS/position-visibility
[11] https://caniuse.com/css-anchor-positioning
[12] https://github.com/mdn/browser-compat-data
[13] https://developer.mozilla.org/en-US/docs/Web/CSS/anchor
[14] https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/position-anchor
[15] https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/popover
[16] https://developer.mozilla.org/en-US/docs/Glossary/Top_layer