CSS Anchor Positioning Is Production-Ready: 5 Patterns
- Tooltips that follow targets with anchor-name and position-area
- Dropdowns and select menus paired with the Popover API for zero JS
- Inline edit popovers that respect viewport edges via position-try
- Context menus from one shared anchor with fallback chains
- Follow-cursor labels in dashboards that survive scroll
- When to pick Anchor Positioning over Floating UI or hand-rolled JS
I have spent more years than I want to count writing JavaScript that calculates where a tooltip should sit. Measure the trigger, measure the viewport, measure the panel, flip when you run out of room, reposition on scroll, reposition on resize, kill it all when the page navigates. Every dropdown, popover, and context menu was the same dance. Floating UI cleaned up a lot of that pain, and I shipped it for years, but it was still a runtime tax of measure-then-paint that sometimes flickered on first frame. Then CSS Anchor Positioning landed in Chrome 125 in May 2024, and by April 2026 Safari 26 and Firefox 127+ both ship the full spec. That changes the calculus. Here are five patterns where I have replaced JavaScript positioning math with twelve lines of CSS, and the one place I still reach for a library.
Tooltips that follow their target
The smallest test case for Anchor Positioning is also the most common UI element in any dashboard. A button, a tooltip, the tooltip needs to sit above the button unless the button is near the top of the viewport, in which case the tooltip flips below. I used to ship that as a fifty-line React hook. Now it is six CSS declarations.
The two primitives are `anchor-name` (declared on the trigger) and `position-anchor` (declared on the floating element). Once both elements know about each other, `position-area` lets you describe the relationship in plain English. `position-area: top` means sit directly above the anchor. `block-start span-inline-end` means sit above and lean to the inline end. The browser handles the math.
.rx-tooltip-trigger {
anchor-name: --tip-anchor;
}
.rx-tooltip {
position: absolute;
position-anchor: --tip-anchor;
position-area: top;
position-try-fallbacks: bottom, left, right;
inset-block-end: anchor(top);
justify-self: anchor-center;
background: #1f1f21;
color: #F5F5F7;
padding: 6px 12px;
border-radius: 9999px;
}
The interesting line is `position-try-fallbacks`. When the tooltip would clip the viewport, the browser walks the list and picks the first option that fits. No measurement loop in user space, no flicker, no `useLayoutEffect`. The work that used to live inside Floating UI's middleware now lives in the rendering engine, which means it runs on the same frame as the rest of layout.
For progressive enhancement I gate the rule behind `@supports (anchor-name: --x)`. Older browsers fall back to a static absolute position with `top: -32px`, which looks fine for the 5 to 8 percent of users still on Chrome 124 or older. If you care about pixel parity in legacy, ship a tiny JS fallback that flips to `bottom` when the trigger is in the upper 100px of the viewport. That is twenty lines of code instead of two hundred.
Dropdowns and select menus
Tooltips are read-only. Dropdowns are interactive, which used to mean focus traps, ARIA wiring, click-outside listeners, and escape-key handlers. The Popover API ships all of that for free, and pairs cleanly with Anchor Positioning. I treat the two as a single pattern now.
The trick is that the `popover` HTML attribute owns the show and hide logic, and Anchor Positioning owns the location. The browser handles the rest, including top-layer rendering (which solves the z-index war I used to fight every quarter).
.rx-select-trigger {
anchor-name: --select-anchor;
}
.rx-select-menu {
position: absolute;
position-anchor: --select-anchor;
position-area: bottom span-inline-end;
inset-block-start: calc(anchor(bottom) + 4px);
min-width: anchor-size(width);
border: 1px solid rgba(245, 245, 247, 0.08);
border-radius: 20px;
background: #1f1f21;
}
.rx-select-menu:popover-open {
display: grid;
}
Two things to call out. First, `anchor-size(width)` lets the menu match the trigger width without JavaScript. I wasted hours of my life writing ResizeObservers for that single behavior. Second, popovers render in the top layer, which means they sit above every stacking context on the page. No more `z-index: 99999` arms races, no more libraries that portal into a sibling of `body` to escape an ancestor's `overflow: hidden`.
Combine this with the new `interesttarget` attribute (Chrome 130+, behind a flag in 125-129) and you get hover-triggered menus with zero JavaScript. For accessibility, the popover attribute already wires up the right ARIA role and the escape-key dismissal. I have shipped four production dropdowns this quarter using this pattern, and all four came in under thirty lines of code per component. For more on how I keep these stacking layers predictable, see my notes on CSS cascade layers.
Inline edit popovers
Inline editing is where positioning libraries earn their keep. The user clicks a cell in a table, a panel opens anchored to that cell, and the panel needs to stay anchored even if the cell scrolls or the row reflows. Floating UI handled this with `autoUpdate`, which polled or used ResizeObserver under the hood. It worked, but on a 200-row table it added enough overhead that I noticed it on a 2018 MacBook.
Anchor Positioning is purely declarative, which means the engine handles updates at the same frequency as layout. No polling, no ResizeObserver, no `requestAnimationFrame` loop. The panel just stays where it should.
.rx-cell-edit {
anchor-name: --cell-anchor;
}
.rx-edit-panel {
position: absolute;
position-anchor: --cell-anchor;
position-area: bottom span-inline-end;
inset-block-start: calc(anchor(bottom) + 8px);
width: 320px;
padding: 16px;
border-radius: 12px;
background: #1f1f21;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.4);
}
@position-try --flip-up {
position-area: top span-inline-end;
inset-block-end: calc(anchor(top) + 8px);
inset-block-start: auto;
}
.rx-edit-panel {
position-try-fallbacks: --flip-up;
}
`@position-try` is the rule I reach for whenever a fallback needs more than a single keyword. You name a position try block, list the declarations that should apply when the primary position fails, and reference it by name in `position-try-fallbacks`. The browser will try the primary, evaluate whether the panel fits in the viewport, and if it does not, swap in the named fallback. You can chain as many fallbacks as you need, and the engine picks the first one that fits.
I pair this with subgrid for the table itself, which means the cells line up across rows even when one row has a panel attached. There is more on that in my writeup of 5 CSS Subgrid patterns, which I treat as required reading before any data table work.
Context menus on long-press or right-click
Context menus are the trickiest of the five patterns because the anchor point is a coordinate, not an element. The user right-clicks at (482, 219) and the menu needs to open there, then flip if it would clip the viewport. The classic trick was a 1px invisible div that I positioned at the click coordinates, then anchored the menu to that div. With Anchor Positioning, that hack still works, and it works really well.
.rx-context-anchor {
position: fixed;
width: 1px;
height: 1px;
pointer-events: none;
anchor-name: --ctx-anchor;
}
.rx-context-menu {
position: absolute;
position-anchor: --ctx-anchor;
position-area: bottom span-inline-end;
inset-block-start: calc(anchor(bottom) + 2px);
min-width: 200px;
padding: 4px 0;
border-radius: 12px;
background: #1f1f21;
}
@position-try --ctx-up-left {
position-area: top span-inline-start;
inset-block-end: calc(anchor(top) + 2px);
inset-inline-end: calc(anchor(left) + 0px);
inset-block-start: auto;
inset-inline-start: auto;
}
.rx-context-menu {
position-try-fallbacks: --ctx-up-left, top, left;
}
The one piece of JavaScript left is the click handler that moves the invisible anchor to the cursor position on `contextmenu` or `pointerup`. Six lines, plus a `popover` attribute on the menu itself for the open and close behavior. Everything that used to be a positioning library is now CSS.
For long-press on touch, I listen for `pointerdown`, start a 350ms timer, and trigger the same handler. The same anchor element, the same menu, the same fallback chain. One UI pattern, two input modes, no extra positioning code. This pattern alone replaced 180 lines of TypeScript in one of my dashboard projects.
Follow-cursor labels in dashboards
The last pattern is the one I was most skeptical about. A scatterplot with hundreds of points, and a label that follows the cursor showing the data for whichever point is hovered. The label needs to stay close to the cursor without obscuring the point, and it needs to flip across quadrants when the cursor approaches the viewport edge.
The naive Anchor Positioning approach fails because the anchor is the cursor, which is not a DOM element. The fix is the same trick as context menus, an invisible 1px element that follows the cursor via a single CSS variable update.
.rx-cursor-tracker {
position: absolute;
width: 1px;
height: 1px;
pointer-events: none;
left: var(--rx-cursor-x, 0);
top: var(--rx-cursor-y, 0);
anchor-name: --cursor-anchor;
}
.rx-cursor-label {
position: absolute;
position-anchor: --cursor-anchor;
position-area: bottom span-inline-end;
inset-block-start: calc(anchor(bottom) + 12px);
inset-inline-start: calc(anchor(right) + 12px);
position-try-fallbacks: top span-inline-start, top span-inline-end, bottom span-inline-start;
padding: 6px 10px;
border-radius: 9999px;
background: rgba(31, 31, 33, 0.92);
color: #F5F5F7;
pointer-events: none;
}
The JavaScript is one `pointermove` handler that updates two CSS variables, throttled with `requestAnimationFrame`. The browser handles the flipping, the scroll-aware repositioning, and the layout. On a 4k display tracking 800 points I get a steady 60fps with this pattern, which I could not hit reliably with Floating UI's `autoUpdate` plus React state. The render path is shorter, and the engine does not need to round-trip through the main thread for a position update.
One detail worth documenting. When the chart container itself scrolls (because it is a long timeline), the cursor anchor moves with the scroll because it lives inside the scrolling parent. The label tracks correctly. With `position: fixed` you would need to adjust for scroll offset by hand. With `position: absolute` inside the scrolling container, anchor positioning just works. For dashboards that lean on this pattern heavily, also see my breakdown of 8 CSS properties for dark UIs for the surface treatment that makes follow-cursor labels readable on top of dense data.
Bottom Line
Anchor Positioning is production-ready as of April 2026. Chrome 125+, Safari 26, and Firefox 127+ all ship the full spec. The remaining gap is users on Chrome 124 or older, which is roughly 5 to 8 percent of traffic depending on your audience. For most apps, ship the CSS and accept that the legacy 5 percent gets a slightly less polished tooltip flip.
Use Anchor Positioning when the floating element has a clear DOM anchor (button, cell, input, hover region with an invisible 1px proxy). Use it when the positioning logic can be expressed as a small set of fallbacks. Use it when you want top-layer rendering and zero z-index management.
Reach for Floating UI when you need positioning behavior that does not map to the spec. Examples include dynamic flipping based on virtual element overlap detection (think of a node-graph editor where labels avoid each other), arrow positioning that depends on the actual chosen fallback, or matrix transforms applied during the position calculation. Floating UI's middleware system handles those cases, and the spec does not yet.
Hand-rolled JavaScript is rarely the right answer in 2026. The two cases I still write it for are anchor-on-canvas (where the anchor is a non-DOM coordinate inside a `
A quick decision tree I now use on every new component. Does the floating element have a clear DOM anchor or a 1px proxy I can place. Yes, use Anchor Positioning. No, ask whether the positioning is interactive at 60fps or one-shot. One-shot, write twenty lines of JS. Interactive, reach for Floating UI. That tree covers maybe 95 percent of the cases I see, and the Anchor Positioning branch wins the vast majority of the time. The tax on that win is roughly two hours of reading the spec and one afternoon converting an existing component to confirm it behaves the way you expect.
The bigger lesson is the same lesson I learn every couple of years. The web platform catches up. Code that I shipped as a library dependency in 2022 is now twelve lines of CSS in 2026. The work is to notice when that happens and ruthlessly delete the old approach instead of carrying both. For a wider tour of the platform features I have moved from JavaScript to CSS this year, the Lab overview is the index I keep updated. Anchor Positioning is the biggest single win on that list, and it deserves a spot in your next greenfield component library.
Back to all articles