I strongly believe Anchor Positioning will go down as one of the greatest additions to CSS. It may not be as game-changing as Flexbox or Grid, but it does fill a positioning gap that has been missing for decades. As awesome as I think it is, CSS Anchor Positioning has a lot of quirks, some of which are the product of its novelty and others due to its unique way of working. Today, I want to bring you yet another Anchor Positioning quirk that has bugged me since I first saw it.
The inception
It all started a month ago when I was reading about what other people have made using Anchor Positioning, specifically this post by Temani Afif about “Anchor Positioning & Scroll-Driven Animations.” I strongly encourage you to read it and find out what caught my eye there. Combining Anchor Positioning and Scroll-Driven Animation, he makes a range slider that changes colors while it progresses.
Amazing by itself, but it’s interesting that he is using two target elements with the same anchor name, each attached to its corresponding anchor, just like magic. If this doesn’t seem as interesting as it looks, we should then briefly recap how Anchor Positioning works.
CSS Anchor Positioning and the anchor-scope
property
See our complete CSS Anchor Positioning Guide for a comprehensive deep dive.
Anchor Positioning brings two new concepts to CSS, an anchor element and a target element. The anchor is the element used as a reference for positioning other elements, hence the anchor name. While the target is an absolutely-positioned element placed relative to one or more anchors.
An anchor and a target can be almost every element, so you can think of them as just two div
sitting next to each other:
<div class="anchor">anchor</div>
<div class="target">target</div>
To start, we first have to register the anchor element in CSS using the anchor-name
property:
.anchor {
anchor-name: --my-anchor;
}
And the position-anchor
property on an absolutely-positioned element attaches it to an anchor of the same name. However, to move the target around the anchor we need the position-area
property.
.target {
position: absolute;
position-anchor: --my-anchor;
position-area: top right;
}
This works great, but things get complicated if we change our markup to include more anchors and targets:
<ul>
<li>
<div class="anchor">anchor 1</div>
<div class="target">target 1</div>
</li>
<li>
<div class="anchor">anchor 2</div>
<div class="target">target 2</div>
</li>
<li>
<div class="anchor">anchor 3</div>
<div class="target">target 3</div>
</li>
</ul>
Instead of each target attaching to its closest anchor, they all pile up at the last registered anchor in the DOM.
The anchor-scope
property was introduced in Chrome 131 as an answer to this issue. It limits the scope of anchors to a subtree so that each target attaches correctly. However, I don’t want to focus on this property, because what initially caught my attention was that Temani didn’t use it. For some reason, they all attached correctly, again, like magic.
What’s happening?
Targets usually attach to the last anchor on the DOM instead of their closest anchor, but in our first example, we saw two anchors with the same anchor-name
and their corresponding targets attached. All this without the anchor-scope
property. What’s happening?
Two words: Containing Block.
Something to know about Anchor Positioning is that it relies a lot on how an element’s containing block is built. This isn’t something inherently from Anchor Positioning but from absolute positioning. Absolute elements are positioned relative to their containing block, and inset properties like top: 0px
, left: 30px
or inset: 1rem
are just moving an element around its containing block boundaries, creating what’s called the inset-modified containing block.
A target attached to an anchor isn’t any different, and what the position-area
property does under the table is change the target’s inset-modified containing block so it is right next to the anchor.
Usually, the containing block of an absolutely-positioned element is the whole viewport, but it can be changed by any ancestor with a position other than static
(usually relative
). Temani takes advantage of this fact and creates a new containing block for each slider, so they can only be attached to their corresponding anchors. If you snoop around the code, you can find it at the beginning:
label {
position: relative;
/* No, It's not useless so don't remove it (or remove it and see what happens) */
}
If we use this tactic on our previous examples, suddenly they are all correctly attached!
Yet another quirk
We didn’t need to use the anchor-scope
property to attach each anchor to its respective target, but instead took advantage of how the containing block of absolute elements is computed. However, there is yet another approach, one that doesn’t need any extra bits of code.
This occurred to me when I was also experimenting with Scroll-Driven Animations and Anchor Positioning and trying to attach text-bubble footnotes on the side of a post, like the following:
Logically, each footnote would be a target, but the choice of an anchor is a little more tricky. I initially thought that each paragraph would work as an anchor, but that would mean having more than one anchor with the same anchor-name
. The result: all the targets would pile up at the last anchor:
This could be solved using our prior approach of creating a new containing block for each note. However, there is another route we can take, what I call the reductionist method. The problem comes when there is more than one anchor with the same anchor-name
, so we will reduce the number of anchors to one, using an element that could work as the common anchor for all targets.
In this case, we just want to position each target on the sides of the post so we can use the entire body of the post as an anchor, and since each target is naturally aligned on the vertical axis, what’s left is to move them along the horizontal axis:
You can better check how it was done on the original post!
Conclusion
The anchor-scope
may be the most recent CSS property to be shipped to a browser (so far, just in Chrome 131+), so we can’t expect its support to be something out of this world. And while I would love to use it every now and there, it will remain bound to short demos for a while. This isn’t a reason to limit the use of other Anchor Positioning properties, which are supported in Chrome 125 onwards (and let’s hope in other browsers in the near future), so I hope these little quirks can help you to keep using Anchor Positioning without any fear.