Popping Comments With CSS Anchor Positioning and View-Driven Animations

Featured Imgs 23

The State of CSS 2024 survey wrapped up and the results are interesting, as always. Even though each section is worth analyzing, we are usually most hyped about the section on the most used CSS features. And if you are interested in writing about web development (maybe start writing with us 😉), you will specifically want to check out the feature’s Reading List section. It holds the features that survey respondents wish to read about after completing the survey and is usually composed of up-and-coming features with low community awareness.

Reading List Results, showing Anchor Positioning and View Driven Animations at the top 5

One of the features I was excited to see was my 2024 top pick: CSS Anchor Positioning, ranking in the survey’s Top 4. Just below, you can find Scroll-Driven Animations, another amazing feature that gained broad browser support this year. Both are elegant and offer good DX, but combining them opens up new possibilities that clearly fall into what most of us would have considered JavaScript territory just last year.

I want to show one of those possibilities while learning more about both features. Specifically, we will make the following blog post in which footnotes pop up as comments on the sides of each text.

For this demo, our requirements will be:

  • Pop the footnotes up when they get into the screen.
  • Attach them to their corresponding texts.
  • The footnotes are on the sides of the screen, so we need a mobile fallback.

The Foundation

To start, we will use the following everyday example of a blog post layout: title, cover image, and body of text:

The only thing to notice about the markup is that now and then we have a paragraph with a footnote at the end:

<main class="post">

  <!-- etc. -->

  <p class="note">
    Super intereseting information!
    <span class="footnote"> A footnote about it </span>
  </p>
</main>

Positioning the Footnotes

In that demo, the footnotes are located inside the body of the post just after the text we want to note. However, we want them to be attached as floating bubbles on the side of the text. In the past, we would probably need a mix of absolute and relative positioning along with finding the correct inset properties for each footnote.

However, we can now use anchor positioning for the job, a feature that allows us to position absolute elements relative to other elements — rather than just relative to the containment context it is in. We will be talking about “anchors” and “targets” for a while, so a little terminology as we get going:

  • Anchor: This is the element used as a reference for positioning other elements, hence the anchor name.
  • Target: This is an absolutely-positioned element placed relative to one or more anchors. The target is the name we will use from now on, but you will often find it as just an “absolutely positioned element” in other resources.

I won’t get into each detail, but if you want to learn more about it I highly recommend our Anchor Positioning Guide for complete information and examples.

The Anchor and Target

It’s easy to know that each .footnote is a target element. Picking our anchor, however, requires more nuance. While it may look like each .note element should be an anchor element, it’s better to choose the whole .post as the anchor. Let me explain if we set the .footnote position to absolute:

.footnote {
  position: absolute;
}

You will notice that the .footnote elements on the post are removed from the normal document flow and they hover visually above their .note elements. This is great news! Since they are already aligned on the vertical axis, we just have to move them on the horizontal axis onto the sides using the post as an anchor.

Example of the footnotes inside the posts and where do we want them

This is when we would need to find the correct inset property to place them on the sides. While this is doable, it’s a painful choice since:

  1. You would have to rely on a magic number.
  2. It depends on the viewport.
  3. It depends on the footnote’s content since it changes its width.

Elements aren’t anchors by default, so to register the post as an anchor, we have to use the anchor-name property and give it a dashed-ident (a custom name starting with two dashes) as a name.

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

In this case, our target element would be the .footnote. To use a target element, we can keep the absolute positioning and select an anchor element using the position-anchor property, which takes the anchor’s dashed ident. This will make .post the default anchor for the target in the following step.

.footnote {
  position: absolute;
  position-anchor: --post;
}

Moving the Target Around

Instead of choosing an arbitrary inset value for the .footnote‘s left or right properties, we can use the anchor() function. It returns a <length> value with the position of one side of the anchor, allowing us to always set the target’s inset properties correctly. So, we can connect the left side of the target to the right side of the anchor and vice versa:

.footnote {
  position: absolute;
  position-anchor: --post;

  /* To place them on the right */
  left: anchor(right);

  /* or to place them on the left*/
  right: anchor(left);

  /* Just one of them at a time! */
}

However, you will notice that it’s stuck to the side of the post with no space in between. Luckily, the margin property works just as you are hoping it does with target elements and gives a little space between the footnote target and the post anchor. We can also add a little more styles to make things prettier:

.footnote {
  /* ... */

  background-color: #fff;
  border-radius: 20px;
  margin: 0px 20px;
  padding: 20px;
}

Lastly, all our .footnote elements are on the same side of the post, if we want to arrange them one on each side, we can use the nth-of-type() selector to select the even and odd notes and set them on opposite sides.

.note:nth-of-type(odd) .footnote {
  left: anchor(right);
}

.note:nth-of-type(even) .footnote {
  right: anchor(left);
}

We use nth-of-type() instead of nth-child since we just want to iterate over .note elements and not all the siblings.

Just remember to remove the last inset declaration from .footnote, and tada! We have our footnotes on each side. You will notice I also added a little triangle on each footnote, but that’s beyond the scope of this post:

The View-Driven Animation

Let’s get into making the pop-up animation. I find it the easiest part since both view and scroll-driven animation are built to be as intuitive as possible. We will start by registering an animation using an everyday @keyframes. What we want is for our footnotes to start being invisible and slowly become bigger and visible:

@keyframes pop-up {
  from {
    opacity: 0;
    transform: scale(0.5);
  }

  to {
    opacity: 1;
  }
}

That’s our animation, now we just have to add it to each .footnote:

.footnote {
  /* ... */
  animation: pop-up linear;
}

This by itself won’t do anything. We usually would have set an animation-duration for it to start. However, view-driven animations don’t run through a set time, rather the animation progression will depend on where the element is on the screen. To do so, we set the animation-timeline to view().

.footnote {
  /* ... */
  animation: pop-up linear;
  animation-timeline: view();
}

This makes the animation finish just as the element is leaving the screen. What we want is for it to finish somewhere more readable. The last touch is setting the animation-range to cover 0% cover 40%. This translates to, “I want the element to start its animation when it’s 0% in the view and end when it’s at 40% in the view.”

.footnote {
  /* ... */
  animation: pop-up linear;
  animation-timeline: view();
  animation-range: cover 0% cover 40%;
}

This amazing tool by Bramus focused on scroll and view-driven animation better shows how the animation-range property works.

What About Mobile?

You may have noticed that this approach to footnotes doesn’t work on smaller screens since there is no space at the sides of the post. The fix is easy. What we want is for the footnotes to display as normal notes on small screens and as comments on larger screens, we can do that by making our comments only available when the screen is bigger than a certain threshold, which is about 1000px. If it isn’t, then the notes are displayed on the body of the post as any other note you may find on the web.

.footnote {
  display: flex;
  gap: 10px;

  border-radius: 20px;
  padding: 20px;

  background-color: #fce6c2;

  &::before {
    content: "Note:";
    font-weight: 600;
  }
}

@media (width > 1000px) {
  /* Styles */
}

Now our comments should be displayed on the sides only when there is enough space for them:

Wrapping Up

If you also like writing about something you are passionate about, you will often find yourself going into random tangents or wanting to add a comment in each paragraph for extra context. At least, that’s my case, so having a way to dynamically show comments is a great addition. Especially when we achieved using only CSS — in a way that we couldn’t just a year ago!


Popping Comments With CSS Anchor Positioning and View-Driven Animations originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

2024: More CSS At-Rules Than the Past Decade Combined

Featured Imgs 23

More times than I can count, while writing, I get myself into random but interesting topics with little relation to the original post. In the end, I have to make the simple but painful choice of deleting or archiving hours of research and writing because I know most people click on a post with a certain expectation of what they’ll get, and I know it isn’t me bombing them with unrelated rants about CSS.

This happened to me while working on Monday’s article about at-rules. All I did there was focus on a number of recipes to test browser support for CSS at-rules. In the process, I began to realize, geez we have so many new at-rules — I wonder how many of them are from this year alone. That’s the rabbit hole I found myself in once I wrapped up the article I was working on.

And guess what, my hunch was right: 2024 has brought more at-rules than an entire decade of CSS.

It all started when I asked myself why we got a selector() wrapper function for the @supports at-rule but are still waiting for an at-rule() version. I can’t pinpoint the exact reasoning there, but I’m certain rthere wasn’t much of a need to check the support of at-rules because, well, there weren’t that many of them — it’s just recently that we got a windfall of at-rules.

Some historical context

So, right around 1998 when the CSS 2 recommendation was released, @import and @page were the only at-rules that made it into the CSS spec. That’s pretty much how things remained until the CSS 2.1 recommendation in 2011 introduced @media. Of course, there were other at-rules like — @font-face, @namespace and @keyframes to name a few — that had already debuted in their own respective modules. By this time, CSS dropped semantic versioning, and the specification didn’t give a true picture of the whole, but rather individual modules organized by feature.

Random tangent: The last accepted consensus says we are at “CSS 3”, but that was a decade ago and some even say we should start getting into CSS 5. Wherever we are is beside the point, although it’s certainly a topic of discussion happening. Is it even useful to have a named version?

The @supports at-rule was released in 2011 in CSS Conditional Rules Module Level 3 — Levels 1 and 2 don’t formally exist but refer to the original CSS 1 and 2 recommendations. We didn’t actually get support for it in most browsers until 2015, and at that time, the existing at-rules already had widespread support. The @supports was only geared towards new properties and values, designed to test browser support for CSS features before attempting to apply styles.

The numbers

As of today, we have a grand total of 18 at-rules in CSS that are supported by at least one major browser. If we look at the year each at-rule was initially defined in a CSSWG Working Draft, we can see they all have been published at a fairly consistent rate:

Number of at-rules per year in FWPD. They all have been added at an average rate of 1 per year, with the highest being 4 in 2021

If we check the number of at-rules supported on each browser per year, however, we can see the massive difference in browser activity:

Number of at-rules per year in FWPD visualized by the browsers that implemented them in a colorful vertical bar chart.

If we just focus on the last year a major browser shipped each at-rule, we will notice that 2024 has brought us a whopping seven at-rules to date!

Numbers of at-rules with support in at least one major browsers. There have been seven that gained support in 2024
Data collected from caniuse.

I like little thought experiments like this. Something you’re researching leads to researching about the same topic; out of scope, but tangentially related. It may not be the sort of thing you bookmark and reference daily, but it is good cocktail chatter. If nothing else, it’s affirming the feeling that CSS is moving fast, like really fast in a way we haven’t seen since CSS 3 first landed.

It also adds context for the CSS features we have — and don’t have. There was no at-rule() function initially because there weren’t many at-rules to begin with. Now that we’ve exploded with more new at-rules than the past decade combined, it may be no coincidence that just last week the Chrome Team updated the function’s status from New to Assigned!

One last note: the reason I’m even thinking about at-rules at all is that we’ve updated the CSS Almanac, expanding it to include more CSS features including at-rules. I’m trying to fill it up and you can always help by becoming a guest writer.


2024: More CSS At-Rules Than the Past Decade Combined originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

CSSWG Minutes Telecon (2024-09-18)

Featured Imgs 23

For the past two months, all my livelihood has gone towards reading, researching, understanding, writing, and editing about Anchor Positioning, and with many Almanac entries published and a full Guide guide on the way, I thought I was ready to tie a bow on it all and call it done. I know that Anchor Positioning is still new and settling in. The speed at which it’s moved, though, is amazing. And there’s more and more coming from the CSSWG!

That all said, I was perusing the last CSSWG minutes telecon and knew I was in for more Anchor Positioning when I came to the following resolution:

Whenever you are comparing names, and at least one is tree scoped, then both are tree scoped, and the scoping has to be exact (not subtree) (Issue #10526: When does anchor-scope “match” a name?)

Resolutions aren’t part of the specification or anything, but the strongest of indications about where they’re headed. So, I thought this was a good opportunity not only to take a peek at what we might get in anchor-scope and touch on other interesting bits from the telecon.

Remember that you can subscribe and read the full minutes on W3C.org. :)

What’s anchor-scope?

To register an anchor, we can give it a distinctive anchor-name and then absolutely positioned elements with a matching position-anchor are attached to it. Even though it may look like it, anchor-name doesn’t have to be unique — we may reuse an anchor element inside a component with the same anchor-name.

<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>

However, if we try to connect them with CSS,

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

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

We get an unpleasant surprise where instead of each .anchor having their .target positioned at its top-right edge, they all pile up on the last .anchor instance. We can see it better by rotating each target a little. You’ll want to check out the next demo in Chrome 125+ to see the behavior:

The anchor-scope property should make an anchor element only discoverable by targets in their individual subtree. So, the prior example would be fixed in the future like this:

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

This is fairly straightforward — anchor-scope makes the anchor element available only in that specific subtree. But then we have to ask another question: What should the anchor-scope own scope be? We can’t have an anchor-scope-scope property and then an anchor-scope-scope-scope and so on… so which behavior should it be?

This is what started the conversation, initially from a GitHub issue:

When an anchor-scope is specified with a <dashed-ident>, it scopes the name to that subtree when the anchor name is “matching”. The problem is that this matching can be interpreted in at least three ways: (Assuming that anchor-scope is a tree-scoped reference, which is also not clear in the spec):

  1. It matches by the ident part of the name only, ignoring any tree-scope that would be associated with the name, or
  2. It matches by exact match of the ident part and the associated tree-scope, or
  3. It matches by some mechanism similar to dereferencing of tree-scoped references, where it’s a match when the tree-scope of the anchor-scope-name is an inclusive ancestor of the tree-scope of the anchor query.

And then onto the CSSWG Minutes:

TabAtkins: In anchor positioning, anchor names and references are tree scoped. The anchor-scope property that scopes, does not say whether the names are tree scoped or not. Question to decide: should they be?

TabAtkins: I think the answer should be yes. If you have an anchor in a shadow tree with a part involved, then problems result if anchor scopes are not tree scoped. This is bad, so I think it should be tree scoped

<khush> sounds pretty reasonable

<fantasai> makes sense to me as far as I can understand it :)

This solution of the scope of scoping properties expanded towards View Transitions, which also rely on tree scoping to work:

khush: Thinking about this in the context of view transitions: in that API you give names and the tree scope has to be the same for them to match. There is another view transitions feature where I’m not sure if the spec says it’s tree scoped

khush: Want to make sure that feature is covered by the more general resolution

TabAtkins: Proposed more general resolution: whenever you are comparing names, and at least one is tree scoped, then both are tree scoped, and the scoping has to be exact (not subtree)

So the scope of anchor-scope is tree-scoped. Say that five times fast!

RESOLVED: whenever you are comparing names, and at least one is tree scoped, then both are tree scoped, and the scoping has to be exact (not subtree)

The next resolution was pretty straightforward. Besides allowing a <dashed-ident> that says that specific anchor is three-scoped, the anchor-scope property can take an all keyword, which means that all anchors are tree-scoped. So, the question was if all is also a tree-scoped value.

TabAtkins: anchor-scope, in addition to idents, can take the keyword ‘all‘, which scopes all names. Should this be a tree-scoped ‘all‘? (i.e. only applies to the current tree scope)

TabAtkins: Proposed resolution: the ‘all‘ keyword is also tree-scoped in the same way sgtm +1, again same pattern with view-transition-group

RESOLVED: the ‘all‘ keyword is tree-scoped

The conversation switched gears toward new properties coming in the CSS Scroll Snap Module Level 2 draft, which is all about changing the user’s initial scroll with CSS. Taking an example directly from the spec, say we have an image carousel:

<div class="carousel">
  <img src="img1.jpg">
  <img src="img2.jpg">
  <img src="img3.jpg" class="origin">
  <img src="img4.jpg">
  <img src="img5.jpg">
</div>

We could set the initial scroll to show another image by setting it’s scroll-start-target to auto:

.carousel {
  overflow-inline: auto;
}

.carousel .origin {
  scroll-start-target: auto;
}

As of right now, the only way to achieve this is using JavaScript to scroll an element into view:

document.querySelector(".origin").scrollIntoView({
  behavior: "auto",
  block: "center",
  inline: "center"
});

The last example is probably a carousel that is only scrollable in the inline direction. Still, there are doubts as far when the container is scrollable in both the inline and block directions. As seen in the initial GitHub issue:

The scroll snap 2 spec says that when there are multiple elements that could be scroll-start-targets for a scroll container “user-agents should select the one which comes first in tree order“.

Selecting the first element in tree-order seems like a natural way to resolve competition between multiple targets which would be scrolled to in one particular axis but is perhaps not as flexible as might be needed for the 2d case where an author wants to scroll to one item in one axis and another item in the other axis.

And back to the CSSWG minutes:

DavidA: We have a property we’re adding called scroll-start-target that indicates if an element within a scroll container, then the scroll should start with that element onscreen. Question is what happens if there are multiple targets?
DavidA: Propose to do it in reverse-DOM order, this would result in the first one applied last and then be on screen. Also, should only change the scroll position if you have to.

After discussing why we have to define scroll-start-target when we have scroll-snap-align, the discussion went on the reverse-DOM order:

fantasai: There was a bunch of discussion about regular vs reverse-DOM order. Where did we end up and why?
flackr: Currently, we expect that it scrolls to the first item in DOM order. We probably want that to still happen. That is why the proposal is to scroll to each item in sequence in reverse-DOM order.

So we are coming in reverse to scroll the element, but only as required so the following elements are showing as much as possible:

flackr: There is also the issue of nearest…
fantasai: Can you explain nearest?
flackr: Same as scroll into view
fantasai: ?
flackr: This is needed with you scroll multiple things into view and want to find a good position (?)
fantasai: You scroll in reverse-DOM order…when you add the spec can you make it really clear that this is the end result of the algorithm?
flackr: Yes absolutely
fantasai: Otherwise it seems to make sense

And so it was resolved:

Proposed resolution 2: When scroll-start-target targets multiple elements, scroll to each in reverse DOM order with text to specify priority is the first item

Lastly, there was the debate about the text-underline-position, that when set to auto says, “The user agent may use any algorithm to determine the underline’s position; however it must be placed at or under the alphabetic baseline.” The discussion was about whether the auto value should automatically adjust the underlined position to match specific language rules, for example, at the right of the text for vertical writing modes, like Japanese and Mongolian.

fantasai: The initial value of text-underline-position is auto, which is defined as “find a good place to put the underline”.
Three options there: (1) under alphabetical baseline, (2) fully below text (good for lots-of-descenders cases), (3) for vertical text on the RHS
fantasai: auto value is defined in the spec about ‘how far down below the text’, but doesn’t say things about flipping. The current spec says “at or below”. In order to handle language-specific aspects, there is a default UA style sheet that for Chinese and Japanese and Korean there are differences for those languages. A couple of implementations do this
fantasai: Should we change the spec to mention these things?
fantasai: Or should we stick with the UA stylesheet approach?

The thing is that Chrome and Firefox already place the underline on the right in vertical Japanese when text-underline-position is auto.

The group was left with three options:

<fantasai> A) Keep spec as-is, update Gecko + Blink to match (using UA stylesheet for language switch)
<fantasai> B) Introduce auto to text-emphasis-position and use it in both text-emphasis-position and text-underline-position to effect language switches
<fantasai> C) Adopt inconsistent behavior: text-underline-position uses ‘auto‘ and text-emphasis-position uses UA stylesheet

Many CSSWG members like Emilio Cobos, TabAtkins, Miriam Suzanne, Rachel Andrew and fantasai casted their votes, resulting in the following resolution:

RESOLVED: add auto value for text-emphasis-position, and change the meaning of text-underline-position: auto to care about left vs right in vertical text

I definitely encourage you to read at the full minutes! Or if you don’t have the time, you can there’s a list just of resolutions.


CSSWG Minutes Telecon (2024-09-18) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

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.