Skip to content
Back to the Lab

CSS Anchor Positioning for Popovers and Tooltips

A deep technical guide to the CSS Anchor Positioning API, covering anchor-name, position-anchor, the anchor() and anchor-size() functions, position-area, custom fallback positions with @position-try, position-visibility, browser support, and a complete dropdown example.

A dark Egnworks banner representing one element anchoring itself to another using CSS.

Positioning a tooltip or dropdown next to the element that triggered it has never been a native CSS problem. A dropdown has to sit below its trigger unless the viewport runs out of room, in which case it has to flip above it. A tooltip has to stay attached to a button that might move if the page scrolls or the layout shifts. Solving this in CSS alone was not possible, so the job fell to JavaScript libraries that read element positions on every scroll and resize event and recalculated coordinates by hand. The CSS Anchor Positioning module replaces that runtime calculation with a set of properties and functions that the layout engine evaluates directly.

Why This Was Always a JavaScript Problem#

CSS positioning ties an element to its containing block, not to an arbitrary element elsewhere in the DOM. A tooltip absolutely positioned inside a card can be placed relative to that card, but it cannot be told to sit below a specific button three levels up the tree without JavaScript computing that button’s coordinates and writing them into inline styles or CSS custom properties.

Libraries such as Popper.js and its successor Floating UI exist specifically to solve this. They attach scroll and resize listeners, measure both elements on every relevant event, run collision detection against the viewport, and write the result back as inline positioning styles. It works, but it adds a runtime dependency, a bundle size cost, and a layer of JavaScript that has to run correctly for every menu, tooltip, and popover on the page.

The Anchor and Its Target#

Anchor positioning introduces two roles. An element becomes an anchor by declaring anchor-name, a custom identifier written as a dashed ident.

.trigger {
  anchor-name: --tooltip-trigger;
}

A separate element, which must be absolutely or fixed positioned, binds to that anchor with position-anchor.

.tooltip {
  position: fixed;
  position-anchor: --tooltip-trigger;
}

Once bound, the tooltip has access to the trigger’s geometry through CSS functions, regardless of where either element sits in the DOM tree or whether they share a positioned ancestor. That last part is what ordinary absolute positioning cannot do: an anchor and its positioned element can live in completely unrelated branches of the document.

Reading Anchor Edges with anchor()#

The anchor() function returns a length by reading a named edge or point of the anchor, and is used inside inset properties such as top, left, bottom, and right.

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

The keyword passed to anchor() can be a physical side (top, bottom, left, right), a logical side (start, end, self-start, self-end), a special keyword (center, inside, outside), or a percentage across the anchor’s box. An optional second argument supplies a fallback length to use if the referenced anchor does not exist, and the anchor name itself can be passed explicitly as the first argument when an element needs to read from an anchor other than the one set by position-anchor. The translate in the example above shifts the tooltip to account for its own width and adds a small gap, since anchor() reports the anchor’s geometry, not the positioned element’s own size.

The Grid Based Alternative: position-area#

Reaching for anchor() on every inset property is precise but verbose for the common case of placing an element on one side of its anchor. position-area covers that case directly by treating the anchor as the center cell of an implicit three by three grid and placing the positioned element on one of the surrounding tiles.

.tooltip {
  position: fixed;
  position-anchor: --tooltip-trigger;
  position-area: top center;
}

top center places the tooltip directly above the trigger, horizontally centered. Physical keywords (top, bottom, left, right, center), logical keywords (block-start, block-end, inline-start, inline-end), and coordinate keywords (x-start, x-end, y-start, y-end) are all valid, and a tile can be widened with span-all or a directional span keyword when the positioned element should straddle more than one cell. For a straightforward side placement, position-area reads more clearly than the equivalent pair of anchor() calls, and the two approaches can be mixed on the same element when one axis needs grid placement and the other needs a precise offset.

Sizing an Element to Match Its Anchor#

anchor-size() mirrors anchor() for dimensions instead of edges, returning a length based on the anchor’s width, height, or the anchor’s own containing block.

.dropdown-panel {
  position: fixed;
  position-anchor: --dropdown-trigger;
  position-area: bottom center;
  width: anchor-size(width);
}

This keeps a dropdown panel exactly as wide as the control that opened it, without JavaScript reading the trigger’s bounding box on open and writing an inline width. anchor-size() accepts the same optional anchor name and fallback length arguments as anchor(), and its result can be used inside calc() the same way, for example doubling the anchor’s width with calc(anchor-size(width) * 2).

Automatic and Custom Fallback Positions#

A tooltip anchored to a button near the bottom of the viewport has nowhere to go if it always renders below that button. position-try-fallbacks gives the browser a list of alternative positions to attempt, in order, whenever the preferred placement would overflow.

.tooltip {
  position: fixed;
  position-anchor: --tooltip-trigger;
  top: anchor(bottom);
  position-try-fallbacks: flip-block;
}

flip-block and flip-inline cover the common case of swapping to the opposite side of the anchor on one axis. When the built-in flip keywords are not specific enough, @position-try defines a named, reusable fallback with its own full set of positioning, sizing, and alignment descriptors.

@position-try --tooltip-left {
  position-area: left center;
  margin-right: 8px;
}

.tooltip {
  position: fixed;
  position-anchor: --tooltip-trigger;
  position-area: top center;
  position-try-fallbacks: --tooltip-left, flip-block;
}

The browser tries --tooltip-left first, then falls back to flip-block, then keeps the original placement if both would still overflow. Values declared inside @position-try take precedence over the base declarations on the element while that fallback is active.

Hiding an Element When Its Anchor Is Not Usable#

Repositioning is not always the right response to an anchor that has scrolled away or disappeared. position-visibility can hide the positioned element entirely, treating it as strongly hidden, equivalent to visibility: hidden on the element and everything inside it.

.tooltip {
  position: fixed;
  position-anchor: --tooltip-trigger;
  position-visibility: no-overflow;
}

anchors-visible, the default, hides the element once its anchor is fully covered or scrolled out of view. no-overflow is stricter, hiding the element as soon as it would itself overflow its containing block or the viewport, even if the anchor is still visible. anchors-valid hides the element if position-anchor no longer points at a real element at all. The specification recommends reaching for position-try-fallbacks first, since keeping the element visible in a different spot is usually better for the user than hiding it outright, and treating position-visibility as the option for cases where no repositioning makes sense.

Scoping Anchor Names#

anchor-name is a plain identifier, which becomes a problem the moment a component with an anchor is rendered more than once on the same page, since every instance would declare the same name. anchor-scope limits which elements a given anchor-name is visible to, so a component repeated in a list can bind each of its own tooltips to its own trigger without the names colliding across instances.

A Complete Dropdown Example#

Combining the pieces above produces a dropdown that matches its trigger’s width, prefers to open downward, and flips upward when the viewport runs out of room below.

.menu-trigger {
  anchor-name: --menu-trigger;
}

@position-try --menu-above {
  position-area: top center;
}

.menu-panel {
  position: fixed;
  position-anchor: --menu-trigger;
  position-area: bottom center;
  width: anchor-size(width);
  margin-top: 4px;
  position-try-fallbacks: --menu-above;
  position-visibility: anchors-visible;
}

Every part of this, including the automatic reflow on scroll, resize, and content changes, runs inside the browser’s layout engine. No script measures either element, and nothing needs to re-run when the page changes shape.

Browser Support and a Path for Older Browsers#

CSS anchor positioning has reached Baseline availability, with full support in Chromium based browsers and Safari, and partial support currently rolling out in Firefox. For a codebase that still needs to support a browser without native support, the OddBird polyfill implements the specification in JavaScript, applied only as a fallback rather than as the primary mechanism, so the CSS itself stays the same regardless of which browser renders it.

What It Does Not Replace#

Anchor positioning solves placement, not disclosure. Deciding whether a popover is open, trapping focus inside it, and closing it on an outside click or an escape key press are still separate concerns, typically handled by the native Popover API or a small amount of application logic. Anchor positioning pairs naturally with the Popover API, since a popover element can be anchored to the control that opened it without either feature depending on the other.

It also does not remove every reason to reach for a JavaScript positioning library. Complex cases such as anchoring to a point that changes anchor entirely based on application state, or coordinating multiple anchored elements that must avoid overlapping each other, are still easier to express in JavaScript today.

When to Reach for It#

SituationRecommendation
A tooltip, dropdown, or simple popover anchored to one trigger elementUse native anchor positioning, since the browser already recalculates position on scroll and resize
A project that must support a browser without native support todayAdd the OddBird polyfill rather than reaching for a full positioning library
A floating element with complex, state driven anchor logicA JavaScript library such as Floating UI may still be simpler to reason about

Conclusion#

CSS anchor positioning takes a problem that used to require a runtime library, tracking one element’s position and size relative to another as the page scrolls and resizes, and moves it into the layout engine itself. Between anchor() for precise offsets, position-area for common side placements, anchor-size() for matching dimensions, and @position-try for custom fallback behavior, the module covers the same ground a positioning library covers today, without a scroll listener or a bundle dependency. With support now broad across major browsers and a polyfill available for the rest, it is a reasonable default for new popovers and tooltips rather than an experimental feature to wait on.

References#

MDN: CSS Anchor Positioning Module

MDN: anchor() Function

MDN: position-area

MDN: anchor-size() Function

MDN: @position-try

MDN: position-visibility

OddBird: CSS Anchor Positioning Polyfill

Can I Use: CSS Anchor Positioning

Last updated