AniUI Academy

CSS Anchor Positioning

Pin a tooltip, popover, or dropdown to another element anywhere in the DOM, with automatic fallback when the preferred position would overflow. No JavaScript measurement loops.

10 min read

Positioning a tooltip under a button sounds like a solved problem. It isn't. For most of CSS's history, "position this floating thing next to that element" was a job that reached for JavaScript — read the anchor's bounding rectangle, measure the viewport, position the tooltip, and re-measure on every scroll and resize. The reason the CSS version couldn't do it: position: absolute is relative to the nearest positioned ancestor, which means the tooltip had to live near the button in the DOM, which means it was subject to overflow: hidden on any ancestor, which is why tooltips get clipped inside cards.

Anchor positioning is the CSS feature that closes that gap. A tooltip can live anywhere in the DOM, be positioned fixed (so it escapes every clipping ancestor and lands in the top layer), and still track a specific target element — by name — as the page scrolls.

The three pieces: name, anchor, and function

<button class="help-button">?</button>
<div class="help-tooltip">This explains the thing.</div>
.help-button {
  anchor-name: --help;
}
 
.help-tooltip {
  position: fixed;
  position-anchor: --help;
 
  top: anchor(bottom);
  left: anchor(left);
  margin-top: 0.5rem;
}

What each piece is doing:

  • anchor-name: --help on the button declares the button as an anchor and gives it a name. The name is a --custom-ident (same kind of thing as a custom property name), not a string.
  • position-anchor: --help on the tooltip opts in to tracking that anchor. The tooltip can be anywhere in the DOM.
  • top: anchor(bottom) says "my top edge should be at the anchor's bottom edge." left: anchor(left) says "my left edge at the anchor's left edge." The result: the tooltip's top-left corner pins to the anchor's bottom-left corner.

Notice what's not in that CSS: no ResizeObserver, no getBoundingClientRect, no scroll listener, no portal. The browser tracks the anchor's position for you.

Why position: fixed matters here

Anchor positioning works with position: absolute and position: fixed. The common choice is fixed for tooltips and popovers, specifically because fixed elements aren't trapped inside clipping ancestors. A tooltip inside a card with overflow: hidden won't be clipped if it's fixed + anchored — it lives at the top of the painting order, visible above the card's clipping boundary, while still following the anchor.

Pair that with the popover attribute (and its own top-layer promotion) and you have the combination that native popovers and menus are built out of:

<button popovertarget="menu" anchor-name="--trigger">Menu</button>
<div popover id="menu" style="position-anchor: --trigger; top: anchor(bottom); left: anchor(left);">
  ...
</div>

The anchor() function, in short

anchor(<edge>) returns the position of one of the anchor's edges. Which value it returns depends on the inset property you're using it in:

  • In top or bottom: a y-axis position.
  • In left or right: an x-axis position.

So the common forms are:

  • top: anchor(bottom) — pin below the anchor.
  • bottom: anchor(top) — pin above (grows upward).
  • left: anchor(right) — pin to the right of (horizontally).
  • right: anchor(left) — pin to the left of.

For the "pin to the center of the anchor" case, there's anchor(center) which works in either axis.

Fallback positions: position-try and position-try-fallbacks

The tooltip placed below a button near the bottom of the viewport would overflow off-screen. This is the classic case for "show above instead" — and until anchor positioning, "show above" was a manual measurement in JavaScript.

CSS now handles it declaratively:

.help-tooltip {
  position: fixed;
  position-anchor: --help;
 
  top: anchor(bottom);
  left: anchor(left);
 
  /* If the preferred position would overflow, try these in order. */
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}

flip-block flips the tooltip across the block axis — if "top: anchor(bottom)" would overflow, try "bottom: anchor(top)" instead. flip-inline flips across the inline axis. Combining them tries "flip both at once." The browser picks the first fallback that fits, or falls back to the preferred position if none fit.

Fallback names also support custom author-defined fallbacks via @position-try, which is how you'd express a more elaborate dance — "prefer below, else above, else to the right, else to the left." For 90% of tooltips, the built-in flip-* keywords are enough.

When this matters, and when not to reach for it

Three shapes of problem where anchor positioning is the honest answer:

  • Tooltips, popovers, and menus that need to track a specific trigger element, escape clipping ancestors, and reposition when they'd overflow.
  • Floating labels or annotations on a diagram or chart where the label needs to stay next to its data point as the layout changes.
  • Custom select dropdowns where the list is portaled into the top layer but needs to align exactly with the control.

Where it's not the right tool:

  • Layout of a component's own structure. That's Flexbox / Grid.
  • Positioning two elements relative to each other when they're natural siblings in the same box. That's also Flexbox / Grid.

The test is "would this previously have been a getBoundingClientRect plus a portal?" If yes, anchor positioning is probably the replacement.

Browser support and progressive enhancement

Anchor positioning is implemented in current Chromium-based browsers and is progressing through Firefox and Safari. Treat it as a feature to reach for with a sensible fallback: a tooltip can still work as an absolutely-positioned child of its button, imperfectly placed, in browsers that haven't shipped anchor positioning yet — and @supports lets you ship both:

.help-tooltip {
  /* Fallback shape: absolutely positioned under the button. */
  position: absolute;
  top: 100%;
  left: 0;
}
 
@supports (anchor-name: --test) {
  .help-button { anchor-name: --help; }
 
  .help-tooltip {
    position: fixed;
    position-anchor: --help;
    top: anchor(bottom);
    left: anchor(left);
    position-try-fallbacks: flip-block;
  }
}

New code targeting the latest Chromium can use it directly; projects with a Firefox/Safari user base should wrap in @supports for now.

What to remember

  • Anchor positioning pins one element to another, by name, regardless of DOM ancestry. It's the CSS answer to "tooltip tracks button, escapes clipping ancestors, re-measures on scroll."
  • Three pieces: anchor-name on the target, position-anchor on the floating element, and anchor(<edge>) inside inset properties (top, left, bottom, right).
  • Pair with position: fixed (and often the popover attribute) so the floating element lives in the top layer and escapes overflow clipping.
  • position-try-fallbacks: flip-block, flip-inline, ... replaces the "measure the viewport and choose above-or-below in JS" pattern — the browser picks the first fallback that fits.
  • Reach for it when the alternative would have been getBoundingClientRect plus a portal. Don't reach for it as a layout tool — Flexbox and Grid still own that.
  • Current Chromium implements it; Firefox and Safari are progressing. Gate with @supports (anchor-name: --test) and keep a sensible fallback.

Check yourself

4 questions · pass 3/4 to unlock Responsive Design and Media Queries

up to 50
  1. 1.What problem does CSS anchor positioning actually solve that position: absolute doesn't?

  2. 2.What does anchor-name do, and how is an element told which anchor to track?

  3. 3.A tooltip is set with top: anchor(bottom). What does that resolve to?

  4. 4.Why do position-try-fallbacks (also exposed via position-try) matter, and what do they do?

4 left to answer

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.