The Browser’s Native “Magnetic Snap” Effect: Demystifying the Anchor Positioning Powerhouse
on , tagged translations, css, anchor-positioning (share this post, e.g., on Mastodon or on Bluesky)
Starting with Chrome 125, a brand-new CSS feature is supported—anchor positioning.
In a previous article, we covered the basics of this new feature in a more systematic way. If you’re interested, check out the earlier post: Get Ahead of the Curve! The Incredibly Powerful Anchor Positioning.
In this article, we’ll use anchor positioning to implement a simplified version of a popover feature. Let’s dive in.
Traditional Popover Behavior
To make a long story short, in day-to-day frontend development, there’s a common scenario where we need to hover over a specific element to pop up an overlay, like this:
![[No image text provided by the source.]](https://d1cxmu1ofnef1v.cloudfront.net/media/posts/anchor-positioning-powerhouse-1.gif)
If we write the popover’s DOM structure inside a specific element—for instance, placing it at the same level as the hovered element—and if one of the ancestor elements of the hovered element has overflow: hidden set, this kind of clipping can occur:
![[No image text provided by the source.]](https://d1cxmu1ofnef1v.cloudfront.net/media/posts/anchor-positioning-powerhouse-2.png)
To avoid this, the common solution is to insert the popover’s DOM under the outermost <body> container of the page, then calculate its position in real time to place the popover near the hovered element. This is essentially what traditional libraries like popper.js, Tippy.js, and others do.
The core logic is as follows:
- Use JavaScript to get the trigger element’s position information (
getBoundingClientRect). - Calculate the remaining viewport space (taking scroll position, window size, and other factors into account).
- Dynamically adjust the popover position (handling boundary collisions, flip logic, and so on).
- Add
resize/scrollevent listeners to correct the position.
The key point is that JavaScript dynamically calculates the current hovered element’s position, places the popover near a suitable spot, and handles all kinds of edge cases. What’s important here is the heavy reliance on JavaScript calculations.
Because traditional CSS has no way to change an element’s positioning parent.
Interestingly, the brand-new CSS feature—anchor positioning—was created to solve exactly this problem.
A Quick Look at Anchor Positioning
So, what exactly is anchor positioning? Let’s get our hands dirty with a demo.
Suppose we have three elements on the page: Button A, Button B, and Positioned Element C, where Element C is inserted anywhere and is absolutely positioned. The core CSS code looks like this:
<body>
<div class="btn-a">Button A</div>
<div class="btn-b">Button B</div>
<div class="anchor">Positioned Element C</div>
</div>.btn-a, .btn-b {
// Various width and height styles
}
.anchor {
position: absolute;
}Right now, the three elements are siblings, and Element C is absolutely positioned relative to body.
The layout looks roughly like this:
![[No image text provided by the source.]](https://d1cxmu1ofnef1v.cloudfront.net/media/posts/anchor-positioning-powerhouse-3.png)
The core purpose of the anchor positioning property is to change the reference for an element’s positioning and enhance its absolute-positioning capabilities. anchor positioning lets us position elements based on the position and size of other anchor elements, rather than in the traditional sense of positioning relative to a parent element.
Next, we’ll use anchor positioning to achieve the following: when Buttons A and B are hovered, Positioned Element C is absolutely positioned relative to the currently hovered button. The core CSS code is as follows:
.btn-a {
// Declare the element as a positioning anchor, named `--btn-a`
anchor-name: --btn-a;
}
.btn-b {
// Declare the element as a positioning anchor, named `--btn-b`
anchor-name: --btn-b;
}
// When the element is hovered, change Element C’s styles
.btn-a:hover ~ .anchor {
// Anchor binding—establish the positioning relationship between the element and the anchor
position-anchor: --btn-a;
// Based on the new anchor element, set the element’s `left` and `top` properties
left: anchor(--btn-a center);
top: anchor(--btn-a bottom);
// Use transform for minor positioning adjustments; non-core code
transform: translate(-50%, 5px);
}
// Similarly, does the same thing as above, only re-setting the anchor element when Button B is hovered
.btn-b:hover ~ .anchor {
position-anchor: --btn-b;
left: anchor(--btn-b center);
top: anchor(--btn-b bottom);
transform: translate(-50%, 5px);
}
.anchor {
// For easier GIF demonstration, add a transition animation to the element; non-core code
transition: all .2s;
}
Focus on the anchor-name, position-anchor, and anchor properties above; their purposes and meanings are explained in the comments and will be described again below.
This way, we’ve achieved the ability to dynamically change Element C’s positioning reference. Let’s see the effect:
![[No image text provided by the source.]](https://d1cxmu1ofnef1v.cloudfront.net/media/posts/anchor-positioning-powerhouse-4.gif)
In short, by leveraging anchor positioning, when hovering over Buttons A and B, we place Element C directly beneath them. That’s the power of anchor positioning!
Core Anchor Positioning APIs
1. anchor-name: Declaring an Anchor
Purpose: Declares an element as a positioning anchor.
Syntax: anchor-name: <dashed-ident>;
Use cases: Trigger elements, reference elements, dynamic positioning sources.
/* Define the trigger element as an anchor */
.trigger-btn {
anchor-name: --menu-anchor;
}2. position-anchor: Binding to an Anchor
Purpose: Establishes a positioning relationship between an element and an anchor.
Syntax: position-anchor: <dashed-ident>;
Note: Must be used with positioning properties (position: fixed/absolute).
.tooltip {
position: fixed;
position-anchor: --menu-anchor; /* Bind to the specified anchor */
}3. anchor(): Dynamic Positioning
Purpose: Calculates coordinates based on the anchor’s position.
Syntax: anchor(<anchor-name>? <anchor-side>)
Directional parameters:
- Vertical:
top/center/bottom - Horizontal:
left/center/right - Combinations:
top-left/bottom-right, and so on
.context-menu {
/* Align to the anchor’s bottom-right corner */
top: anchor(--ctx-anchor bottom);
left: anchor(--ctx-anchor right);
}
.tooltip {
/* Horizontally centered relative to the anchor */
left: anchor(center);
right: anchor(center);
}4. anchor-size(): Inheriting Dimensions
Purpose: Gets the anchor element’s dimension values.
Syntax: anchor-size(<anchor-name>? <dimension>)
Dimension parameters: width/height/block/inline
.popover {
/* Inherit the anchor’s width */
width: anchor-size(width);
/* Minimum height is 1.5× the anchor’s height */
min-height: calc(anchor-size(height) * 1.5);
}These are the most core properties of anchor positioning. Mastering them will let you handle most scenarios. In my previous introductory article, they are described in even more detail; interested readers can check it out: Get Ahead of the Curve! The Incredibly Powerful Anchor Positioning.
Fallback Positions in Anchor Positioning
There’s another very important point: Traditional popover components generally have a feature—smart boundary handling.
Let’s take the fairly powerful popover library floating-ui as an example. Its official site demonstrates the following feature: when an element scrolls, if the original popover popup gets obscured, it automatically shifts position and re-adjusts the popup into the visible area. The effect looks like this:
![[No image text provided by the source.]](https://d1cxmu1ofnef1v.cloudfront.net/media/posts/anchor-positioning-powerhouse-5.gif)
Excitingly, CSS anchor positioning now supports this same smart boundary handling—in anchor positioning, we call it fallback positions.
Here, we mainly rely on two anchor positioning–related properties to achieve fallback positions: position-try-fallbacks and the @position-try rule.
The @position-try Rule
@position-try is used to define an alternative positioning strategy (a set of positioning rules) that can be reused across multiple elements. Its syntax is similar to defining a named rule set.
/* Syntax example: */
@position-try --strategy-name {
/* Specific positioning rules */
top: anchor(bottom);
left: anchor(left);
}Key points:
- Named strategy: Each
@position-tryrule needs a unique name (e.g.,--tooltip-below). - Independent scope: The positioning rules inside a strategy are independent of the element’s own styles and only take effect when invoked.
The position-try-fallbacks Property
position-try-fallbacks is used on an element to specify the priority order of alternative positioning strategies. The browser tries these strategies in order until it finds the first viable position.
/* Syntax example: */
.element {
position-try-options: --strategy1, --strategy2, --strategy3;
}Key points:
- Order-sensitive: The browser tries strategies in list order; the first viable strategy is applied.
- Dynamic fallback: If all strategies are unviable, the element falls back to its default positioning (or parent-container constraints).
Implementing Fallback Positions With Anchor Positioning
Based on the above, let’s implement fallback positions using anchor positioning.
Suppose we have the following structure, already using anchor positioning:
<body>
<div class="btn">Reference</div>
<div class="anchor">Popover Element</div>
</div>The core CSS is as follows:
.btn {
anchor-name: --btn;
border: 1px dashed #000;
background: #ddd;
}
.anchor {
position: absolute;
position-anchor: --btn;
left: anchor(--btn-a center);
top: anchor(--btn-a bottom);
transform: translate(-50%, 5px);
}At this point, both elements are inserted under <body>, but the popover element uses position-anchor: --btn for anchor positioning, making its absolutely positioned parent .btn, and it’s positioned below the button. The effect looks like this:
![[No image text provided by the source.]](https://d1cxmu1ofnef1v.cloudfront.net/media/posts/anchor-positioning-powerhouse-6.png)
Now, we just need to leverage position-try-fallbacks and @position-try to implement fallback positions:
@position-trydefines a fallback rule.position-try-fallbacksbrings in the fallback rule.
The core CSS code is as follows:
.btn {
anchor-name: --btn;
border: 1px dashed #000;
background: #ddd;
}
.anchor {
position: absolute;
position-anchor: --btn;
left: anchor(--btn-a center);
top: anchor(--btn-a bottom);
transform: translate(-50%, 5px);
position-try-fallbacks: --position-bottom;
}
@position-try --position-bottom {
left: anchor(--btn center);
bottom: anchor(--btn top);
top: unset;
margin-bottom: 10px;
}This way, as we scroll the page, if the popover exceeds the viewport, the fallback rule automatically takes effect. Let’s see the result:
![[No image text provided by the source.]](https://d1cxmu1ofnef1v.cloudfront.net/media/posts/anchor-positioning-powerhouse-7.gif)
Look closely—the effect is identical to the smart positioning implemented with the JavaScript library above, except this time we only used a few lines of CSS code! Amazing!
To sum up, today we can already use CSS anchor positioning to roughly implement a minimal popover popup that satisfies most scenarios. One can’t help but marvel at how increasingly powerful CSS has become.
Of course, the anchor positioning features introduced in this article are all based on implementing a minimal version of a popover. Beyond the Anchor Positioning API and fallback positions, there’s much more interesting content to explore. Interested readers can dive into MDN: MDN—Anchor Positioning, MDN—@position-try.
Finally
Alright, that’s a wrap for this article—a very interesting CSS technique. I hope you found it helpful. 😃
If you want to get the most interesting CSS news, don’t miss my WeChat public account—iCSS Frontend Tidbits 😄
More great CSS technical articles are collected on my GitHub—iCSS, continuously updated. Feel free to star and subscribe.
If you have any questions or suggestions, let’s chat more. This is an original article; my writing skills are limited, and my knowledge is shallow. If there are any inaccuracies in the text, please let me know.
(This post is a machine-made, human-reviewed, and authorized translation of cnblogs.com/coco1s/p/18739625.)