Design · October 3, 2026
CSS Anchor Positioning Changes Component APIs: The Browser Now Owns Overlay Geometry
By Anika Sarder · Digital Marketing Specialist
Photo by Mediamodifier on Unsplash
The overlay problem is moving out of JavaScript
CSS anchor positioning is not merely a shorter way to place a tooltip. It changes which layer of a frontend system owns the relationship between an interactive control and the floating content attached to it. The browser can now keep an overlay connected to an anchor, choose a fallback when space disappears, and expose the chosen fallback back to CSS.
That matters because overlay geometry has traditionally been treated as application state. A component renders a trigger, measures it with getBoundingClientRect(), calculates coordinates, installs scroll and resize listeners, and updates classes when the tooltip must flip. The component is responsible for both meaning—“this popover belongs to this button”—and mechanics—“its top edge is 12 pixels below that button unless the viewport is too short.”
The CSS Anchor Positioning Module Level 1 moves the second responsibility into the platform. Its model is explicit: a positioned box can size and position itself relative to one or more anchor boxes, then try alternate positions to avoid overlap or overflow. The correct architectural response is not to replace every positioning utility overnight. It is to stop designing overlay components around coordinates and start designing them around relationships.
What the browser can own now
The new model has three pieces: an anchor relationship, a placement rule, and a fallback policy. Keeping those pieces separate is the key to using the feature without turning CSS into an opaque magic trick.
An anchor declares a name; a positioned element references it. The positioned element can then use either a nine-cell position-area grid or the more precise anchor() and anchor-size() functions. The basic relationship looks like this:
.trigger {
anchor-name: --account-trigger;
}
.account-menu {
position: fixed;
position-anchor: --account-trigger;
position-area: block-end span-inline;
margin-block-start: 0.5rem;
}
The position-area approach is the right default for common menus, tooltips, and action panels. It describes intent—below the anchor, spanning its inline axis—instead of encoding a viewport coordinate. When the component needs an exact edge relationship, anchor() returns a length that can be used inside calc() or clamp():
.account-menu {
position: fixed;
position-anchor: --account-trigger;
inset-block-start: anchor(--account-trigger block-end);
inset-inline-start: anchor(--account-trigger inline-start);
max-inline-size: min(24rem, calc(100vi - 2 * 1rem));
}
The browser is also able to size one element from another. MDN’s anchor positioning guide documents anchor-size() as a way to use an anchor dimension inside sizing and inset values. That is useful for a select-like panel or a command menu that should initially match its trigger width without a measurement pass:
.command-menu {
inline-size: max(16rem, anchor-size(--account-trigger inline-size));
}
This is a design-system change. A menu component no longer needs a prop called triggerRect, a context object containing top and left, or an imperative “recalculate position” method. Its public contract can expose an anchor relationship and a placement preference instead.
The real win is fallback policy, not fewer lines of CSS
Most overlay bugs happen at the boundary of the viewport, not in the happy path. A tooltip looks correct in the middle of a page and fails when its anchor is near the top, inside a scroll container, or rendered at a narrow width. JavaScript libraries solve this with a loop: measure, test, mutate, and measure again.
CSS anchor positioning gives the browser an ordered set of alternatives. The position-try-fallbacks property can use predefined flips, position-area values, or custom @position-try rules. The browser tries them in order and keeps the first result that avoids the relevant overflow condition.
.tooltip {
position: fixed;
position-anchor: --help-anchor;
position-area: block-start;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
max-inline-size: min(20rem, calc(100vi - 2rem));
}
The combined fallback is important. If a tooltip is above and to the left of a corner anchor, flipping only the block axis may still leave it outside the viewport. MDN’s fallback guide shows why individual options and a combined flip-block flip-inline option should be treated as distinct candidates. A fallback list is not a bag of suggestions; it is a deterministic priority order.
For a component library, that priority order should be part of the component’s design contract. A useful policy table might look like this:
| Component | Preferred placement | First fallback | Final behavior |
|---|---|---|---|
| Tooltip | Block-start | flip-block | Hide when the anchor is not visible |
| Context menu | Inline-end | flip-inline | Try the opposite corner, then constrain width |
| Select menu | Block-end | flip-block | Prefer the side with more available height |
| Persistent inspector | Inline-end | Custom @position-try | Remain visible, but become scrollable |
This is more maintainable than giving every component a generic “placement” string and hoping a positioning library’s defaults fit the interaction. The UI pattern determines whether flipping, shrinking, or hiding is the least surprising outcome.
CSS can now react to the fallback it selected
Level 1 fallback behavior solves placement but creates a subtle visual problem: the component may move without knowing that it moved. A tooltip arrow that points upward in its default position will point away from the anchor after a flip. A menu’s attached corner may have the wrong radius. A gradient or shadow may imply the wrong direction.
Chrome’s documentation for anchored container queries describes the missing feedback channel. Set container-type: anchored on the positioned element, then query the fallback that the browser selected:
.tooltip {
container-type: anchored;
position: fixed;
position-anchor: --help-anchor;
position-area: block-end;
position-try-fallbacks: flip-block;
}
.tooltip::before {
content: "";
position: absolute;
inset-block-start: -0.4rem;
border: 0.4rem solid transparent;
border-block-end-color: var(--tooltip-bg);
}
@container anchored(fallback: flip-block) {
.tooltip::before {
inset-block-start: auto;
inset-block-end: -0.4rem;
border-block-end-color: transparent;
border-block-start-color: var(--tooltip-bg);
}
}
The important architectural distinction is that CSS is reacting to a layout decision, not reimplementing the decision in JavaScript. There is no second collision algorithm to drift from the browser’s algorithm. The arrow, radius, and decorative attachment all consume the same result.
This also explains why anchor positioning should not be wrapped in an abstraction that hides every CSS detail. Component authors need a vocabulary for placement and fallback state. A useful API might offer placement="block-start" and collision="flip-and-hide", then compile those options into a documented set of CSS rules. The API names product behavior; CSS owns the geometry.
Popover makes the relationship semantic
Anchor positioning becomes particularly powerful when combined with the Popover API. A button connected to a popover through popovertarget creates an implicit anchor relationship. The trigger and the floating element are no longer related only because a component passed two DOM references through JavaScript; the HTML expresses the relationship directly.
<button popovertarget="profile-menu" id="profile-trigger">
Account
</button>
<div id="profile-menu" popover>
<a href="/settings">Settings</a>
<button popovertarget="profile-menu" popovertargetaction="hide">Close</button>
</div>
#profile-menu {
position-area: block-end span-inline;
margin: unset;
position-try: flip-block;
}
The margin: unset is not cosmetic. As web.dev documents, popovers are centered by default with margin: auto; that default must be removed when the popover is positioned relative to its implicit anchor. The popover also participates in the top layer, so the component does not need to win a z-index contest with unrelated content.
There are limits. A popover is not a modal dialog: the page behind it is not inert. It has no inherent semantic role merely because it has a popover attribute, and manual popovers leave more dismissal behavior to the author. The web.dev popover guide distinguishes auto, manual, and the proposed or available hint behavior and explains how their stacks interact.
The practical rule is simple: use Popover for the interaction and accessibility primitives it actually provides; use anchor positioning for the geometry; use dialog when the background must become inert. Combining the APIs is better than forcing one primitive to impersonate another.
Repeated components need anchor scoping
The first serious design-system trap is name collision. Anchor names are not automatically local to a component instance. If a page renders a list of rows and every trigger declares anchor-name: --row-trigger, an unscoped positioned element can resolve to the wrong visible anchor. MDN explicitly calls out the repeated-component case and recommends anchor-scope to limit which names are visible within a subtree.
A reusable component should therefore treat anchor names like any other identifier with a scope policy:
.menu-item {
anchor-scope: --row-trigger;
}
.menu-item__trigger {
anchor-name: --row-trigger;
}
.menu-item__menu {
position-anchor: --row-trigger;
position: fixed;
position-area: block-end span-inline;
}
The exact placement of the scope depends on the component’s DOM and whether the positioned element remains in the same subtree. The point is to decide deliberately. This is analogous to the native CSS architecture approach described in our earlier analysis: once the browser supplies a primitive, the system still needs naming, layering, and ownership rules around it.
A second trap is assuming that every anchor follows the same scroll behavior. The W3C draft describes special handling for scrolling and notes that transforms can introduce delayed updates in some implementations. web.dev also warns that when anchors exist in different scroll containers, the positioned element follows only its default anchor for scrolling. If a component crosses scroll boundaries, test that case explicitly instead of declaring the JavaScript implementation obsolete by fiat.
Accessibility remains an HTML responsibility
Anchor positioning changes visual geometry; it does not create or repair accessibility relationships. The W3C specification’s accessibility section is clear that CSS does not alter the bindings between elements. A tooltip still needs a meaningful relationship such as aria-describedby. A menu still needs the correct semantic structure, keyboard behavior, and focus policy. A popover still needs the right choice between a non-modal popover and a modal dialog.
This separation is healthy. It means the design system can test the semantic contract independently from the placement policy:
- Can a keyboard user discover, open, navigate, and dismiss the overlay?
- Does the trigger expose the relationship to assistive technology?
- Does the overlay remain usable when the browser chooses every configured fallback?
- Does it disappear when the anchor is no longer meaningfully visible?
position-visibility: no-overflow and position-visibility: anchors-visible can prevent detached content from remaining on screen. They are useful safety policies, not substitutes for focus management. If hiding the positioned element leaves focus inside it, the interaction still needs an explicit focus strategy.
A migration plan for existing component libraries
Do not start by deleting the positioning library. Start by classifying overlay behavior. The migration is safer when geometry, interaction, and semantics are separated first.
1. Inventory the measurement code. Search for getBoundingClientRect(), ResizeObserver, scroll listeners, viewport collision helpers, and props named placement, strategy, offset, or referenceRect. Record which code measures, which code decides, and which code only applies styles.
2. Move the relationship into markup. For popovers, use a declarative invoker where it fits. For other overlays, create a stable anchor name and a scoped association. Keep JavaScript responsible for opening state and content; stop passing pixel coordinates through the component tree.
3. Replace the happy-path coordinate with position-area. Begin with one preferred position. Add position-try-fallbacks only after you have written down the desired priority order. This avoids migrating an accidental behavior that was never part of the product requirement.
4. Add the final-state styling. If the arrow, border radius, or animation needs to know whether a fallback was selected, use an anchored container query where support permits. Otherwise, retain a narrowly scoped fallback class rather than rebuilding the entire collision engine.
5. Test the boundaries, not just screenshots. Exercise each overlay near every viewport edge, inside nested scrolling containers, with zoom, long localized strings, keyboard focus, reduced motion, and repeated instances. Test a missing or hidden anchor as well. The browser has taken ownership of geometry, so your tests must verify the browser’s decisions against the component contract.
The browser is ready to own more of overlay geometry, but the design system still owns the policy. The winning architecture is not “CSS replaces JavaScript.” It is a smaller JavaScript layer for state and semantics, a CSS layer for relationships and fallback presentation, and an explicit component contract that says what should happen when space runs out.
The decision: adopt relationships, keep escape hatches
CSS anchor positioning is ready for progressive adoption in components where the anchor relationship is stable and the fallback policy is understandable. It is not a reason to remove every mature positioning dependency from a large application in one sprint. Browser support can be uneven across subfeatures, and complex cross-scroll or transformed layouts deserve a measured rollout.
The right near-term boundary is clear: keep JavaScript for behavior that CSS cannot express—focus transitions, async content, business rules, and analytics. Remove JavaScript whose only purpose is to repeatedly rediscover where one DOM element is relative to another. Let the browser select a valid position, let CSS style the selected state, and make the component’s accessibility contract independent of both.
That is the architectural shift. An overlay is no longer a rectangle that an application has to chase. It is content with an anchor, a placement preference, and a policy for failure.