CSS anchor positioning ties an absolutely positioned element to another element’s box, so a dropdown, tooltip or popover tracks its trigger without JavaScript measuring anything. You declare anchor-name on the trigger, position-anchor on the panel, and position-area for placement. Firefox 147 shipped it on 13 January 2026, which put it in all three engines.
Chrome had it first, in 125, back in May 2024. Safari followed in 26 in September 2025. So if your support floor is “current versions of Chrome, Firefox and Safari”, this is available to you now, and the third-party positioning library in your bundle has become optional.
What does CSS anchor positioning actually replace?
Floating UI (@floating-ui/dom, 1.8.0 as of August 2026, and the successor to Popper.js) does four jobs. It measures the reference element and the floating one. It applies a placement. It flips or shifts that placement when the panel would fall outside the viewport. Then it re-runs all of it on every scroll and resize through autoUpdate.
CSS now covers three of those in the layout engine. Placement is position-area or the anchor() function. Flipping is position-try-fallbacks. Tracking is free, because the position is resolved during layout instead of in an animation frame callback you own.
The measuring is the job you stop needing at all.
That is a performance argument as much as a bundle-size one. Every autoUpdate tick reads layout from JavaScript, and reading layout inside a scroll handler is the well-worn route to a dropped frame. Pushing the work into CSS takes it off the path that shows up when you are tracking down a bad Interaction to Next Paint score.
The smallest dropdown that works
Pair it with the Popover API and there is no JavaScript in the component at all. A popover opened by popovertarget gets an implicit anchor reference to the button that opened it, so you can skip anchor-name and position-anchor entirely. That implicit link landed in Chrome 133, Firefox 147 and Safari 26.
<button popovertarget="account-menu">Account</button>
<div popover id="account-menu">
<a href="/settings">Settings</a>
<a href="/billing">Billing</a>
<a href="/sign-out">Sign out</a>
</div>
#account-menu {
/* The UA stylesheet centres popovers in the viewport. Undo that first,
or the insets fight your placement and nothing appears to happen. */
inset: auto;
margin: 0;
position-area: block-end span-inline-start;
margin-block-start: 0.5rem;
min-width: anchor-size(width);
max-width: 20rem;
}
position-area places the panel on an implicit 3x3 grid drawn around the anchor. block-end span-inline-start means “below it, extending towards the inline start edge”, which in a left-to-right document is a menu hanging down and left-aligned. anchor-size(width) reads the button’s width, so the menu is never narrower than its trigger.
Outside a popover you need the explicit form: anchor-name: --account-trigger on the button, position-anchor: --account-trigger plus position: absolute on the panel.
How do you keep it inside the viewport?
position-try-fallbacks takes an ordered list. The browser lays the element out with the first option, checks whether it overflows, and works down the list until something fits.
You can pass position-area values directly, or the tactics flip-block, flip-inline and flip-start, which mirror the current placement across the anchor. flip-x and flip-y came later: Chrome 144, Firefox 147, Safari 26.2.
For anything more than a mirror, name a block with @position-try. It accepts inset properties, margins, sizing, self-alignment, position-anchor and position-area.
@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;
}
#account-menu {
position-try-fallbacks: --menu-above, flip-inline;
position-try-order: most-block-size;
}
position-try-order changes the tie-break. normal keeps your declared order. most-block-size, most-inline-size, most-width and most-height instead pick whichever option leaves the element the most room, which is what you want for a long menu near the bottom of a scrolling page. Firefox got that property in 148, released 24 February 2026, a release behind the rest of the feature.
Why does the top layer matter here?
The reason teams reach for a positioning library is rarely the maths. It is that the trigger sits inside a container with overflow: hidden, or inside a stacking context that traps z-index, so an absolutely positioned panel gets clipped. React answers that with a portal. Vue answers it with a teleport. Both mean the panel is no longer next to its trigger in the DOM, which is why you then need JavaScript to reunite them.
A popover is put in the top layer when it opens, above every stacking context and outside every overflow clip, with no portal involved. The anchor stays where it was in the markup.
There is a second reason to use popover rather than a plain <div>, and it is the one that shows up in WCAG compliance work: you get light dismiss on outside click, Esc to close, and the guarantee that opening one auto popover closes the last one, all from the attribute.
The top layer also sidesteps a rule that catches people out. Per the spec, an anchor must be “laid out strictly before” the element it positions, which for two elements sharing a containing block means earlier in tree order. Put your tooltip markup above its trigger in the source and it silently anchors to nothing. A popover is in a higher top layer than the page, so the ordering question never arises.
What is still rough in August 2026
Exit transitions are the sharpest edge. transition-behavior: allow-discrete and @starting-style are everywhere (Firefox 129, Safari 17.4 and 17.5), but the overlay property that keeps an element in the top layer until its animation finishes is still Chromium-only. Your fade-out plays in Chrome and gets cut short elsewhere.
[popover] {
opacity: 0;
transition:
opacity 0.2s,
overlay 0.2s allow-discrete,
display 0.2s allow-discrete;
}
[popover]:popover-open { opacity: 1; }
@starting-style {
[popover]:popover-open { opacity: 0; }
}
position-visibility is still moving. Chrome, Firefox and Safari 26.2 all take always, anchors-visible and no-overflow, but anchors-valid is Firefox and Safari only, and Safari 27 renames both of those to the singular anchor-valid and anchor-visible. Stick to the three that agree.
popover="hint", the state built for hover tooltips that should not close an open menu, is Chrome 151 and Firefox 153 with nothing shipped in Safari.
And watch the property names in older material. Chromium shipped inset-area before renaming it to position-area in 129, and position-try-options before position-try-fallbacks in 128. Any tutorial written between May and August 2024 uses names that no browser accepts today.
What about browsers without it?
Feature-query it and treat the anchored version as the upgrade. A dropdown pinned below its trigger with plain absolute positioning is a perfectly usable fallback; it just will not flip.
.menu {
position: absolute;
inset-block-start: 100%;
inset-inline-start: 0;
}
@supports (anchor-name: --probe) {
.menu {
inset: auto;
position-area: block-end span-inline-start;
position-try-fallbacks: flip-block, flip-inline;
}
}
There is a polyfill, @oddbird/css-anchor-positioning, at 0.10.2. It is still pre-1.0 and it parses your stylesheets at runtime, so it costs you more than the library you were trying to delete. Use it only if you have a contractual support floor that reaches back past Safari 26.
On a new build with an evergreen support floor, I would not install a positioning library at all. Write the popover, write the position-area, add two fallbacks, ship it. The same thing happened with page transitions once cross-document view transitions landed natively: the platform absorbed a job the ecosystem had been doing in JavaScript.
Where I would leave Floating UI alone: if you are already on Radix, Headless UI or MUI, the library is in your bundle regardless and removing your own call sites saves nothing. Keep it too if you position against a virtual element, a text selection, a cursor position, a point on a canvas, because there is no element to name and anchor positioning has no answer for that at all.