•5 min read

CSS Anchor Positioning: Ditch Your Tooltip Library

Last week I deleted Floating UI from a client dashboard and replaced it with about twenty lines of CSS. The tooltips there had been misbehaving for months. One attached itself to the wrong button after a filter re-render, another hung half off the right edge of the viewport. Every fix added more positioning code I did not want to own. I had put off CSS anchor positioning because I assumed browser support was still a mess. It isn't anymore, and honestly the setup turned out smaller than the library config I removed.

Why tooltips needed a library in the first place

Here is the root problem. When you set position: absolute on a tooltip, it resolves against the nearest positioned ancestor, not the button you actually care about. Two elements that are not parent and child had no way to talk to each other in CSS. So JavaScript did the talking. Measure the trigger with getBoundingClientRect(), add scroll offsets, write the coordinates back, then redo all of it on every scroll and resize event. And if the tooltip lands near the viewport edge, flip it to the other side.

That last part is why Popper.js and Floating UI exist. The math is fiddly, the edge cases multiply, and doing it badly is very visible. Fair enough. But the whole mess came from one missing link: CSS had no way to say "this element belongs over there, next to that element". Anchor positioning is exactly that link.

Hand-drawn wireframe sketches of web page layouts on paper

Two elements and a name

You name an anchor, then you point another element at it. The names use the same double-dash convention as custom properties.

<button class="trigger">Options</button>

<div class="tooltip">Saved to drafts</div>
.trigger {
  anchor-name: --trigger;
}

.tooltip {
  position: absolute;
  position-anchor: --trigger;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% 0;
  margin-bottom: 8px;
}

That is a working, centered tooltip with zero JavaScript. The two elements do not need to be siblings, share a parent, or have any DOM relationship at all. The browser resolves the tether during layout, so it keeps working when the anchor moves, when the page scrolls, when a filter re-renders the toolbar. No listeners to clean up. The full anchor() syntax lives on MDN. anchor(center) gives you the midpoint on that axis, and since the result is a plain length, calc() works.

position-area covers the common cases

Writing inset properties by hand gets old fast. position-area is the higher-level version for standard placements. Picture a three by three grid around the anchor and pick a cell.

.tooltip {
  position: absolute;
  position-anchor: --trigger;
  position-area: top center;
  margin-bottom: 8px;
}

span values work too: bottom span-right puts the element below the anchor and lets it stretch across everything to the right, which is what you want for a dropdown menu. Logical values like block-start inline-end resolve correctly in right-to-left layouts, something my old code never did.

Flipping at the edge with @position-try

This is the part you actually imported a library for: the tooltip wants to sit above the button, but the button is near the top of the screen. @position-try declares fallback placements as plain CSS, and the browser walks the list until one fits.

@position-try --flip-bottom {
  position-area: bottom center;
  margin-top: 8px;
  margin-bottom: 0;
}

.tooltip {
  position: absolute;
  position-anchor: --trigger;
  position-area: top center;
  margin-bottom: 8px;
  position-try-fallbacks: --flip-bottom, flip-inline;
}

All of that resolves during the layout pass. flip-block and flip-inline are built-in shorthands for the common single-axis flips, so half the time you don't even need a named rule.

No middleware. No update() call on every scroll.

Popovers, support, and what the tutorials skip

Anchor positioning only places things. For toggling, Escape handling, light dismiss, and floating above every z-index fight, pair it with the popover attribute. I covered the dialog element doing a heavier version of this before; popovers are the lighter sibling. One thing that bit me: popovers ship with browser default centering styles, so reset those before you anchor.

.menu {
  inset: auto;
  margin: 0;
  position: absolute;
  position-anchor: --options;
  position-area: bottom span-right;
  width: anchor-size(width);
}

width: anchor-size(width) makes the menu exactly as wide as its trigger. Small detail, but it is what makes a dropdown actually look attached.

Support is finally boring. Chrome and Edge got it in mid-2024, Safari last September, and Firefox shipped the final piece this past January, which made the feature Baseline. We are at roughly ninety percent of browsers now. In an older one, the anchored element just renders in its static position instead of tethering. For a tooltip that is a cosmetic downgrade, not a broken page. If you need pixel-identical behavior everywhere, keep your library. I don't.

Three traps from my notes. Tutorials from before mid-2025 call position-area "inset-area"; that name is dead, and a lot of articles still use it. For repeated components, say a table where every row is an anchor with the same name, set anchor-scope on the container so a tooltip can only tether to the anchor inside its own row. And animating a popover in and out still needs @starting-style plus transition-behavior: allow-discrete, because the element enters from display: none and a plain transition never fires there.

I kept Floating UI in exactly one spot: a calendar where events get positioned against a time range rather than a real DOM node. Virtual elements are still its home turf. But every plain tooltip, menu, and popover in the codebase now runs through the browser's own layout engine, at native speed, and it cannot fall out of sync with the DOM because it is the DOM. After a decade of measuring rectangles by hand, that feels right.

The flip patterns and the popover reset now sit in my snippet manager, next to the container query and :has() recipes. That is where all this platform CSS goes for me: three lines today, a whole dependency gone tomorrow. Snippet Ark keeps it all local, which suits me fine.