Lesson 25 of 37
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.
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: --helpon 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: --helpon 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
toporbottom: a y-axis position. - In
leftorright: 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-nameon the target,position-anchoron the floating element, andanchor(<edge>)inside inset properties (top,left,bottom,right). - Pair with
position: fixed(and often thepopoverattribute) 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
getBoundingClientRectplus 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
1.What problem does CSS anchor positioning actually solve that
position: absolutedoesn't?2.What does
anchor-namedo, and how is an element told which anchor to track?3.A tooltip is set with
top: anchor(bottom). What does that resolve to?4.Why do
position-try-fallbacks(also exposed viaposition-try) matter, and what do they do?
4 left to answer
Discussion
Sign in to postNo comments yet. Be the first to say something.