Anchor positioning: the end of JavaScript libraries for tooltips and menus
93 million downloads a week to compute the position of a tooltip. Since January 2026, anchor positioning is supported by every major engine and the browser does it on its own. Building a tooltip and then a full menu in native CSS, fallbacks included, followed by the strategy for removing the dependency without breaking older browsers.

93 million downloads in a single week. That is the volume measured on August 7, 2026 for Floating UI (opens in a new tab), the reference JavaScript library for positioning tooltips, dropdown menus and popovers. Its predecessor Popper (opens in a new tab), despite having been replaced by Floating UI, still adds 24 million weekly downloads. All that volume to answer a question that sounds trivial: where should this element sit relative to that one.
Since January 2026, that question has a native answer. With Firefox 147 (opens in a new tab), the last engine to implement it, CSS anchor positioning reached Baseline (opens in a new tab), the status granted when a feature becomes available in every major browser. Three CSS properties are now enough to attach a tooltip to its button, keep it in place while scrolling and flip it to the other side when space runs out. Without JavaScript.
The topic deserves more than a syntax demo. This article starts with what these libraries actually compute. That computation explains why so many tooltips end up clipped by a screen edge. It then builds a tooltip and a full menu in native CSS, fallbacks included for when space runs out. It ends with the question that matters in production. Who can remove the dependency today and who still needs to keep it?
What Floating UI computes on your behalf
Positioning a tooltip below its button with an eight pixel gap sounds simple. The difficulty was never there. In CSS, an element with position: absolute is placed relative to its nearest positioned ancestor, not relative to an arbitrary element on the page. To pin a tooltip to a button, there were two compromises to choose from. Nested inside the button, it inherits its parents' overflow and ends up clipped by the first container that crops its content. Moved to the root of the document, it escapes clipping but nothing ties it to the button anymore, so coordinates have to be measured in JavaScript.
The ecosystem industrialized that second path. This is what the contract looks like:
import { computePosition, offset, flip, shift } from '@floating-ui/dom';
function position() {
computePosition(button, tooltip, {
placement: 'top',
middleware: [offset(8), flip(), shift({ padding: 8 })],
}).then(({ x, y }) => {
Object.assign(tooltip.style, {
left: `${x}px`,
top: `${y}px`,
});
});
}Positioning a tooltip with Floating UI
The function computes a position once. Yet a position is never settled: the visitor scrolls, the window resizes, an intermediate container scrolls too, content shifts. So the library has to run that computation again and again, listening to the scroll and resize of every scrollable ancestor. Each run reads dimensions with getBoundingClientRect then writes styles, the exact read-write cycle that feeds layout thrashing (opens in a new tab) on the main thread.
That contract costs on three fronts. The base module weighs 8.2 KB gzipped (opens in a new tab), before any component built on top. Its listeners work during scrolling, the worst possible moment to keep the browser busy. And its reliability depends on an endless list of edge cases, an ancestor with a transform that shifts the reference frame, a sticky container, a zoom level, an iframe. Every tooltip ever seen frozen in the wrong place after a scroll is one of those edge cases that slipped through the computation.
Three properties that replace the computation
Anchor positioning inverts the model. Instead of measuring coordinates, a relationship is declared: this element is anchored to that one. The browser maintains the relationship itself, every frame, whatever the scrolling. The full tooltip:
.button {
anchor-name: --button;
}
.tooltip {
position: fixed;
position-anchor: --button;
position-area: block-start;
margin-block-end: 8px;
}An anchored tooltip in native CSS
anchor-name declares the anchor, with a double-dash name like a CSS variable. position-anchor ties the positioned element to that anchor. position-area places it on an imaginary grid of nine cells centered on the button, three rows by three columns, the middle cell being the button itself. block-start targets the top row, so the tooltip shows above, centered. The margin-block-end recreates the eight pixel gap, the equivalent of the offset(8) middleware.
Two details condition the behavior. The positioned element must use position: fixed or absolute, otherwise the anchoring properties are ignored. And the anchor must be rendered before the positioned element in document order, a constraint that rarely bites for a tooltip placed right after its button in the HTML.
| Intent | Logical value | Physical value |
|---|---|---|
| Above, centered | block-start | top |
| Below, centered | block-end | bottom |
| To the right, centered | inline-end | right |
| Below, aligned with the button's left edge | block-end span-inline-end | bottom span-right |
The four most common placements of an anchored element.
For pixel-level control rather than cell-level, the anchor() function exposes the anchor's edges directly inside inset properties:
.tooltip {
position: fixed;
position-anchor: --button;
bottom: anchor(top);
left: anchor(left);
}Fine-grained control with the anchor() function
bottom: anchor(top) glues the bottom of the tooltip to the top of the button, left: anchor(left) aligns their left edges. Both approaches combine with calc() for fine offsets. As for scroll tracking, it requires nothing, since the browser repositions the anchored element itself. What used to demand listeners, measurements and style writes on every frame becomes a guarantee of the rendering engine. The MDN guide (opens in a new tab) details the full set of available values.
The full menu: popover for the other half of the problem
Positioning was only half the work of a dropdown menu. The other half consists of sitting above everything else on the page. A menu nested inside a position: sticky header with a modest z-index will end up behind an element with a more aggressive stacking or clipped by an overflow: hidden. The HTML popover attribute settles that half by promoting the element to the top layer, a layer above the whole document that no z-index and no overflow can interfere with. Combined with anchoring:
<button popovertarget="menu" class="trigger">
Options
</button>
<nav id="menu" popover>
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
<a href="/logout">Log out</a>
</nav>A dropdown menu without JavaScript
.trigger {
anchor-name: --menu;
}
#menu {
position-anchor: --menu;
position-area: block-end span-inline-end;
width: anchor-size(width);
margin: 8px 0 0;
}The CSS of the anchored menu
The browser provides the behavior: click the button to open, click elsewhere or press Escape to close, stacking management if several popovers open. anchor-size(width) matches the menu's width to the button's, a select-menu classic that still required a JavaScript measurement. The margin: 8px 0 0 resets the popover's default margins while keeping the vertical gap.
A point of honesty is in order. popover handles opening and closing, not the semantics of a keyboard-driven menu. A real application menu, with arrow-key navigation and ARIA roles, still needs JavaScript for its behavior. What disappears is the positioning JavaScript, the fragile and costly part.
When space runs out: declarative fallbacks
That leaves the most visible service of the libraries, flipping the tooltip when it overflows the screen. A tooltip configured above a button sitting at the top of the page must switch below. That is Floating UI's flip() middleware. The declarative version:
.tooltip {
position-area: block-start;
position-try-fallbacks: flip-block, flip-inline;
}Flipping when space runs out
position-try-fallbacks lists backup positions that the browser tries in order as soon as the main position overflows the visible area. flip-block mirrors the placement on the vertical axis, flip-inline on the horizontal axis. For a fallback more specific than a simple mirror, @position-try declares a complete alternative position:
@position-try --to-the-right {
position-area: inline-end;
width: anchor-size(width);
}
#menu {
position-area: block-end span-inline-end;
position-try-fallbacks: flip-block, --to-the-right;
}A custom fallback position
Last piece, position-visibility: anchors-visible hides the positioned element when its anchor leaves the screen, the expected behavior for a tooltip whose button has scrolled out of view. Together this covers most of what the flip, shift and hide middlewares did in JavaScript, in a syntax the browser optimizes itself.
What JavaScript still does better
Honesty demands the reverse inventory. Three cases keep JavaScript ahead.
- Following the cursor or an arbitrary point. A context menu that opens where the click happened has no anchor element: Floating UI accepts virtual anchors, plain coordinates. CSS requires a real element in the document
- Custom collision logic.
position-try-fallbackschooses among positions declared upfront. Finer logic, such as avoiding a specific area of the interface or sliding continuously along an axis, remains middleware territory - Components from third-party libraries. Bootstrap requires Popper (opens in a new tab) for its dropdowns and most React component kits ship Floating UI as a transitive dependency. That dependency will not leave through a local refactoring: it will leave when those projects migrate, update after update. That is what the 93 million downloads actually tell: very few developers install the library themselves, it arrives in the luggage of a component
Adopting it today, without breaking anything
Support arrived in three steps, Chrome and Edge 125 (opens in a new tab) in May 2024, Safari 26 (opens in a new tab) on September 15, 2025, then Firefox 147 (opens in a new tab) in January 2026. As of August 7, 2026, Caniuse (opens in a new tab) measures about 82% of global coverage. The exact status remains "Newly available", as opposed to the "Widely available" label that Baseline only grants after thirty months of widespread support. In other words, a real share of traffic still browses with earlier versions. Three strategies cover that reality, from the lightest to the most cautious.
The degraded fallback is a direct application of progressive enhancement (opens in a new tab). Recent browsers get the anchored tooltip, the others a simplified version, positioned in the flow below the trigger or replaced by the title attribute. The test fits in one feature query:
@supports (anchor-name: --x) {
.tooltip {
position: fixed;
position-anchor: --button;
position-area: block-start;
position-try-fallbacks: flip-block;
}
}Progressive enhancement with @supports
The polyfill when the experience must be identical everywhere. OddBird (opens in a new tab) maintains a polyfill that reads the anchoring properties in stylesheets and emulates them in JavaScript on browsers that ignore them. Loaded conditionally, it weighs nothing for recent browsers:
<script type="module">
if (!("anchorName" in document.documentElement.style)) {
import("https://unpkg.com/@oddbird/css-anchor-positioning");
}
</script>Loading the polyfill only when needed
The monitored status quo applies to components that come from a third-party design system, whose migration belongs to the upstream project, not to the project consuming it. The useful move is rather to stop adding the library for one's own needs. The next in-house tooltip does not need eight kilobytes of JavaScript.
The decision comes down to this:
- New project or mainstream audience on recent browsers: native with an
@supportsfallback, zero dependency - Aging browser fleet, intranet, public sector: native plus polyfill, the experience stays identical everywhere
- Cursor-following context menu or custom collision logic: Floating UI remains the right tool
- Components from a third-party kit: wait for its updates and install nothing new
The browser always catches up eventually
Anchor positioning follows a familiar trajectory. jQuery handed its selectors over to querySelector, JavaScript carousels handed scrolling over to scroll-snap and scroll-driven animations are being rewritten in native CSS (opens in a new tab). At every iteration, a layer of JavaScript that had become plumbing returns to the browser, faster and more reliable than the best library implementation, because it runs inside the rendering engine rather than on top of it. Developers are not mistaken about it. In the State of CSS 2026 (opens in a new tab) survey, anchor positioning shows the largest year-over-year usage increase of all tracked features (+15%).
This movement draws a healthy division of roles. The plumbing, measuring, tracking, flipping, goes back to the browser. The design work remains whole, deciding what a menu reveals, when a tooltip helps instead of cluttering, how an interface responds to the hand exploring it. A site stands out through that second kind of work. Might as well stop paying for the first.