Anchor Positioning Quirks

Featured Imgs 23

I am thrilled to say, that from this week onwards, the CSS-tricks Almanac has an entry for each property, function, and at-rule related to the new Anchor Positioning API! For the last month, I have tried to fully understand this new module and explain it to the best of my ability. However, anchor positioning is still a new feature that brings even newer dynamics on how to position absolute elements, so it’s bound to have some weird quirks and maybe even a few bugs lurking around.

To celebrate the coverage, I wanted to discuss those head-scratchers I found while diving into this stuff and break them down so that hopefully, you won’t have to bang your head against the wall like I did at first.

The inset-modified containing block

A static element containing block is a fairly straightforward concept: it’s that element’s parent element’s content area. But things get tricky when talking about absolutely positioned elements. By default, an absolutely positioned element’s containing block is the viewport or the element’s closest ancestor with a position other than static, or certain values in properties like contain or filter.

All in all, the rules around an absolute element’s containing block aren’t so hard to remember. While anchor positioning and the containing block have their quirks (for example, the anchor element must be painted before the positioned element), I wanted to focus on the inset-modified containing block (which I’ll abbreviate as IMCB from here on out).

There isn’t a lot of information regarding the inset-modified containing block, and what information exists comes directly from the anchor positioning specification module. This tells me that, while it isn’t something new in CSS, it’s definitely something that has gained relevance thanks to anchor positioning.

The best explanation I could find comes directly from the spec:

For an absolutely positioned box, the inset properties effectively reduce the containing block into which it is sized and positioned by the specified amounts. The resulting rectangle is called the inset-modified containing block.

So if we inset an absolutely positioned element’s (with top, left, bottom, right, etc.), its containing block shrinks by the values on each property.

.absolute {
  position: absolute;
  top: 80px;
  right: 120px;
  bottom: 180px;
  left: 90px;
}

For this example, the element’s containing block is the full viewport, while its inset modified containing block is 80px away from the top, 120px away from the right, 180px away from the bottom, and 90px away from the left.

Example of an inset-modified containing block. It's shrinked 80px from the top, 120px from the right, 180px from the bottom and 90px from the left

Knowing how the IMCB works isn’t a top priority for learning CSS, but if you want to understand anchor positioning to its fullest, it’s a must-know concept. For instance, the position-area and position-try-order heavily rely on this concept.

In the case of the position-area property, a target containing block can be broken down into a grid divided by four imaginary lines:

  1. The start of the target’s containing block.
  2. The start of the anchor element or anchor(start).
  3. The end of the anchor element or anchor(end).
  4. The end of the target’s containing block.
Example of how we can think of the containing block of an anchor element as a 3x3 asymmetrical grid

The position-area property uses this 3×3 imaginary grid surrounding the target to position itself inside the grid. So, if we have two elements…

<div class="anchor">Anchor</div>
<div class="target">Target</div>

…attached with anchor positioning:

.anchor {
  anchor-name: --my-anchor;

  height: 50px;
  width: 50px;
}

.target {
  position: absolute;
  position-anchor: --my-anchor;

  height: 50px;
  width: 50px;
}

…we can position the .target element using the position-area property:

.target {
  position: absolute;
  position-anchor: --my-anchor;
  position-area: top left;

  height: 50px;
  width: 50px;
}

The IMCB is shrunk to fit inside the region of the grid we selected, in this case, the top-left region.

Example of the inset-modified containing block of a target element at the top left of the anchor

You can see it by setting both target’s dimensions to 100%:

The position-try-order also uses the IMCB dimensions to decide how to order the fallbacks declared in the position-try-fallbacks property. It checks which one of the fallbacks provides the IMCB with the largest available height or width, depending on whether you set the property with either the most-height or most-width values.

I had a hard time understanding this concept, but I think it’s perfectly shown in a visual tool by Una Kravets on https://chrome.dev/anchor-tool/.

Specification vs. implementation

The spec was my best friend while I researched anchor positioning. However, theory can only take you so far, and playing with a new feature is the fun part of understanding how it works. In the case of anchor positioning, some things were written in the spec but didn’t actually work in browsers (Chromium-based browsers at the time). After staring mindlessly at my screen, I found the issue was due to something so simple I didn’t even consider it: the browser and the spec didn’t match.

Anchor positioning is different from a lot of other features in how fast it shipped to browsers. The first draft was published on June 2023 and, just a year later, it was released on Chrome 125. To put it into perspective, the first draft for custom properties was published in 2012 and we waited four years to see it in implemented in browsers (although, Firefox shipped it years before other browsers).

I am excited to see browsers shipping new CSS features at a fast pace. While it’s awesome to get new stuff faster, it leaves less space between browsers and the CSSWG to remake features and polish existing drafts. Remember, once something is available in browsers, it’s hard to change or remove it. In the case of anchor positioning, browsers shipped certain properties and functions early on that were ultimately changed before the spec had fully settled into a Candidate Recommendation.

It’s a bit confusing, but as of Chrome 129+, this is the stuff that Chrome shipped that required changes:

position-area

The inset-area property was renamed to position-area (#10209), but it will be supported until Chrome 131.

.target {
  /* from */
  inset-area: top right;

  /* to */
  position-area: top right;
}

position-try-fallbacks

The position-try-options was renamed to position-try-fallbacks (#10395).

.target {
  /* from */
  position-try-options: flip-block, --smaller-target;

  /* to */
  position-try-fallbacks: flip-block, --smaller-target;
}

inset-area()

The inset-area() wrapper function doesn’t exist anymore for the position-try-fallbacks (#10320), you can just write the values without the wrapper

.target {
  /* from */
  position-try-options: inset-area(top left);

  /* to */
  position-try-fallbacks: top left;
}

anchor(center)

In the beginning, if we wanted to center a target from the center, we would have to write this convoluted syntax

.target {
  --center: anchor(--x 50%);
  --half-distance: min(abs(0% - var(--center)), abs(100% - var(--center)));
	
  left: calc(var(--center) - var(--half-distance));
  right: calc(var(--center) - var(--half-distance));
}

The CWSSG working group resolved (#8979) to add the anchor(center) argument for much-needed brevity.

.target {
  left: anchor(center);
}

Bugs!

Some bugs snuck into browser implementations of qnchor positioning. For example, the spec says that if an element doesn’t have a default anchor element, then the position-area property does nothing. This is a known issue (#10500) but it’s still possible to replicate, so please, just don’t do it.

The following code…

.container {
  position: relative;
}

.element {
  position: absolute;
  position-area: center;
  margin: auto;
}

…centers the .element inside its container as we can see in this demo from Temani Afif:

Another example comes from the position-visibility property. If your anchor element is off-screen, you typically want its target to be hidden as well. The spec says the default is anchors-visible, but browsers go with always instead.

Chrome currently isn’t reflecting the spec. It indeed is using always as the initial value. But the spec’s text is intentional — if your anchor is off-screen or otherwise scrolled off, you usually want it to hide (#10425).

Anchor positioning accessibility

While anchor positioning’s most straightforward use case is for stuff like tooltips, infoboxes, and popovers, it can be used for a lot of other stuff as well. Check this example by Silvestar Bistrović, for example, where he connects elements with lines. He’s tethered elements together for decorative purposes, so anchor positioning doesn’t mean there is a semantic relationship between the elements. As a consequence, non-visual agents, like screen readers, are left in the dark about how to interpret two seemingly unrelated elements.

If we’re aiming to link a tooltip to another element, we need to set up a relationship in the DOM and let anchor positioning handle the visuals. Happily, there are APIs (like the Popover API) that do this for us, even establishing an anchor relationship that we can take advantage of to create more compelling visuals.

In a general way, the spec describes an approach to create this relationship using ARIA attributes such as the aria-details or aria-describedby, along the role attribute on the target element.

So, while we could attach the following two elements…

<div class="anchor">anchor</div>
<div class="toolip">toolip</div>

…using anchor positioning:

.anchor {
  anchor-name: --my-anchor;
}

.toolip {
  position: absolute;
  position-anchor: --my-anchor;
  position-area: top;
}

…but screen readers only see two elements next to one another without any remarked relationship. That’s a bummer for accessibility, but we can easily fix it using the corresponding ARIA attribute:

<div class="anchor" aria-describedby="tooltipInfo">anchor</div>
<div class="toolip" role="tooltip" id="tooltipInfo">toolip</div>

And now they are both visually and semantically linked together! It would just be better if could pull it off without ARIA.

Conclusion

Being confused by a new feature just to finally understand it is one of the most satisfying experiences anyone in programming can feel. While there are still some things about anchor positioning that can be (and are) confusing, I’m pleased to say the CSS-Tricks Almanac now has a deluge of information to help clarify things.

The most exciting thing is that anchor positioning is still in an early stage. That means there are many more confusing things coming for us to discover and learn!


Anchor Positioning Quirks originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.