CSS anchor positioning lets you tether tooltips, dropdowns, and menus to any element using three CSS properties — anchor-name, position-anchor, and position-area. It replaces JavaScript positioning libraries like Floating UI and Popper.js. As of 2026, it works in every major browser.
If you have Floating UI in your package.json, you’re about to question that dependency. Here’s a tooltip without JavaScript:
.trigger { anchor-name: --tip; }
.tooltip {
position: fixed;
position-anchor: --tip;
position-area: top;
}
Three anchor-positioning declarations. No computePosition(). No resize observer. No scroll listener. CSS anchor positioning hit Baseline 2026 — Chrome 125+, Firefox 147+, Safari 26+ — replacing libraries with 23 million combined weekly npm downloads.
“Three properties” is the pitch, though. Real tooltips flip at viewport edges, need fallback positions, and ship with progressive enhancement. Here’s the full migration — what maps cleanly, what needs workarounds, and when to keep JavaScript.
What You’re Replacing
This is what a Floating UI tooltip costs:
import { computePosition, flip, offset } from '@floating-ui/dom';
function update() {
computePosition(trigger, tooltip, {
placement: 'top',
middleware: [offset(8), flip()],
}).then(({ x, y }) => {
tooltip.style.left = `${x}px`;
tooltip.style.top = `${y}px`;
});
}
update();
addEventListener('scroll', update, true);
addEventListener('resize', update);
An import. Twelve lines of positioning logic. Two event listeners that fire on every scroll and resize, each calling getBoundingClientRect() and mutating the DOM. The flip() middleware detects viewport overflow and repositions. The offset() adds spacing. And all of it runs in JavaScript, on the main thread, every single frame.
Here’s the CSS equivalent:
.trigger { anchor-name: --tip; }
.tooltip {
position: fixed;
position-anchor: --tip;
position-area: top;
margin-bottom: 8px;
}
Six declarations. No imports. No event listeners. The browser recalculates position during its normal layout pass — the one it’s running anyway.
The concepts map directly:
| Floating UI | CSS Anchor Positioning |
|---|---|
placement: 'top' |
position-area: top |
flip() middleware |
position-try-fallbacks: flip-block |
offset(8) |
margin-bottom: 8px |
autoUpdate() |
Nothing — browser handles it |
The performance difference is structural. Floating UI runs JavaScript on every scroll frame — getBoundingClientRect(), position math, DOM writes. CSS anchor positioning resolves during layout. No JS execution. No requestAnimationFrame loops. No layout thrashing.
That covers placement and offset. But Floating UI’s flip() middleware — the part that repositions your tooltip when it hits the viewport edge — is what most codebases actually depend on.
Flip, Fallback, Done
One line handles it:
position-try-fallbacks: flip-block;
flip-block flips the tooltip vertically when it runs out of viewport space. flip-inline flips horizontally. Chain both for full coverage: flip-block flip-inline. That single declaration replaces Floating UI’s flip middleware, its detection logic, and the recalculation loop that powers it.
When you need more control than a directional flip, @position-try defines custom fallback positions:
@position-try --to-right {
position-area: right;
margin-left: 8px;
}
.tooltip {
position-area: top;
position-try-fallbacks: flip-block, --to-right;
}
The browser evaluates each fallback in order and picks the first one that fits. If you’d rather maximize space instead of following a fixed order, position-try-order: most-height lets the browser choose whichever position gives the tooltip the most room — useful when tooltip content varies in length.
One more property worth knowing: position-visibility: anchors-visible hides the tooltip entirely when its anchor scrolls out of view. That replaces yet another scroll listener you were about to write.
Three CSS features cover what Floating UI needs a middleware stack, two observers, and a positioning loop to achieve. But reading about properties is one thing — let’s see what production patterns actually look like.
Three Patterns You Ship Today
Tooltip with Arrow
.trigger { anchor-name: --tip; }
.tooltip {
position: fixed;
position-anchor: --tip;
position-area: top;
margin-bottom: 12px;
position-try-fallbacks: flip-block;
}
.tooltip::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
translate: -50% 0;
border: 6px solid transparent;
border-top-color: var(--tooltip-bg);
}
The arrow is a pseudo-element CSS triangle, positioned relative to the tooltip’s bottom edge. One caveat worth knowing: when flip-block flips the tooltip below the trigger, the ::after arrow doesn’t automatically reverse direction. @position-try blocks only affect the anchored element itself — not its children or pseudo-elements.
Two clean options. Use a subtle box-shadow instead of a triangle arrow — most modern tooltip systems have moved this direction anyway, and it sidesteps the problem entirely. Or add a small JS observer that detects the resolved position and toggles an arrow class. For shipping fast, the shadow approach wins on simplicity.
Dropdown Menu — Zero JavaScript
<button class="trigger" popovertarget="menu">Options ▾</button>
<ul id="menu" popover class="dropdown">
<li><a href="/settings">Settings</a></li>
<li><a href="/logout">Log out</a></li>
</ul>
.trigger { anchor-name: --menu; }
.dropdown {
position: fixed;
position-anchor: --menu;
position-area: bottom;
position-try-fallbacks: flip-block;
}
popovertarget handles open and close on click — no toggle handler needed. Anchor positioning handles placement. position-try-fallbacks handles viewport edges. And the Popover API adds light-dismiss (click outside to close) and top-layer stacking for free. No z-index wars. No click-outside detection. This replaces every dropdown positioning library you’ve ever installed. Combined with CSS view transitions, the entire tooltip/dropdown lifecycle — show, position, animate, dismiss — runs without a single JavaScript event handler.
Form Validation with Enter Animation
.input { anchor-name: --field; }
.error-msg {
position: fixed;
position-anchor: --field;
position-area: right;
margin-left: 8px;
opacity: 0;
transition: opacity 0.2s, display 0.2s allow-discrete;
}
.error-msg:popover-open {
opacity: 1;
@starting-style {
opacity: 0;
}
}
@starting-style tells the browser what values to transition from when the element enters the page. The error message fades in smoothly when the popover opens — no animation library, no requestAnimationFrame. The transition on the base rule handles the exit fade when it closes. One CSS block replaces what used to take a JS animation library and a positioning library working in tandem.
Three production patterns, zero runtime JavaScript. But before you run npm uninstall — the browser support question.
Ship It: Progressive Enhancement
Baseline 2026 means Chrome 125+, Firefox 147+, Safari 26+. That covers roughly 92% of global browser traffic as of April 2026. For a feature that went from first spec draft to cross-browser shipping in under three years, the adoption curve has been unusually fast.
For the other 8%, wrap your anchor positioning in @supports:
/* Fallback: position relative to parent */
.tooltip {
position: absolute;
bottom: 100%;
left: 50%;
translate: -50% -8px;
}
/* Enhancement: anchor positioning */
@supports (anchor-name: --x) {
.tooltip {
position: fixed;
position-anchor: --tip;
position-area: top;
bottom: auto;
left: auto;
translate: none;
margin-bottom: 8px;
}
}
Browsers without support get a traditional absolutely-positioned tooltip. Browsers with support get the anchored version with flip behavior. Nobody gets a broken layout. The same progressive enhancement pattern works for container queries — browsers without support get a reasonable fallback, browsers with support get the enhanced version.
For teams that need wider coverage, the Oddbird polyfill weighs roughly 8 KB gzipped and supports Chrome 51+, Firefox 54+, and Safari 10+. Load it conditionally — don’t ship polyfill bytes to browsers that don’t need them:
if (!CSS.supports('anchor-name', '--x')) {
import('@oddbird/css-anchor-positioning');
}
One Safari note: versions 18.2 through 18.3 shipped core anchor positioning properties but not @position-try. Safari 18.4+ and Safari 26 have full support including fallbacks. If you’re supporting those older Safari versions, your tooltips render correctly in their default position — they just won’t flip at viewport edges. That’s acceptable degradation for most production apps.
You’ve got the patterns and the shipping strategy. One question left: should you migrate everything to CSS?
When to Keep JavaScript
No. CSS anchor positioning doesn’t cover every positioning scenario, and pretending otherwise wastes your time. Here’s where JavaScript is still the right tool:
Shadow DOM. anchor-name doesn’t cross shadow boundaries. If your web components use shadow DOM, CSS can’t reference an anchor declared in a different shadow tree. You’ll need JS positioning inside those components.
Virtualized lists. Anchors must exist in the DOM. If you’re using virtual scrolling and the anchor element gets removed when it leaves the rendered window, CSS has nothing to anchor to. Floating UI handles this because it works with coordinates, not DOM relationships.
Complex nested menus. Multi-level flyout menus where sub-menus position relative to parent items in shifting directions — technically possible with cascading @position-try blocks, but the complexity grows fast. A JavaScript coordinator is cleaner and easier to debug.
Dynamic anchor switching. Guided tours, contextual help, or any pattern where a tooltip jumps between different trigger elements based on user interaction — JavaScript handles the reassignment more directly than swapping CSS custom properties at runtime.
These aren’t hypothetical edge cases. They’re specific architectural patterns. If your codebase uses them, you already know it.
So — can you actually delete the library?
The Delete Checklist
Open your package.json. Here’s your answer.
If your positioned elements are tooltips, dropdowns, popovers, or menus — delete the library. CSS anchor positioning handles all of these with less code, zero runtime cost, and no main-thread JavaScript on scroll. The migration is a net reduction in complexity by every measure.
If you have shadow DOM components, virtualized lists, or complex nested menus — keep the library for those specific cases. Migrate the simple patterns to CSS first. Floating UI and CSS anchor positioning coexist without conflict — use each where it’s strongest.
You don’t need a rewrite sprint. Pick one tooltip. Replace the JS with three CSS properties. Ship it. Then do the next one. The migration is incremental by nature because each component is independent.
The best part isn’t the bundle savings. It’s the scroll listeners, resize observers, and positioning functions you never write again.