CSS Anchor Positioning Guide

Featured Imgs 23

Not long ago, if we wanted a tooltip or popover positioned on top of another element, we would have to set our tooltip’s position to something other than static and use its inset/transform properties to place it exactly where we want. This works, but the element’s position is susceptible to user scrolls, zooming, or animations since the tooltip could overflow off of the screen or wind up in an awkward position. The only way to solve this was using JavaScript to check whenever the tooltip goes out of bounds so we can correct it… again in JavaScript.

CSS Anchor Positioning gives us a simple interface to attach elements next to others just by saying which sides to connect — directly in CSS. It also lets us set a fallback position so that we can avoid the overflow issues we just described. For example, we might set a tooltip element above its anchor but allow it to fold underneath the anchor when it runs out of room to show it above.

Anchor positioning is different from a lot of other features as far as how quickly it’s gained browser support: its 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 specification for CSS variables was published in 2012, but it took four years for them to gain wide browser support.

So, let’s dig in and learn about things like attaching target elements to anchor elements and positioning and sizing them.

Quick reference

/* Define an anchor element */
.anchor {
  anchor-name: --my-anchor;
}
/* Anchor a target element */
.target {
  position: absolute;
  position-anchor: --my-anchor;
}
/* Position a target element */
.target { 
  position-area: start end;
}

Basics and terminology

At its most basic, CSS Anchor Positioning introduces a completely new way of placing elements on the page relative to one another. To make our lives easier, we’re going to use specific names to clarify which element is connecting to which:

  • 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 the spec.

For the following code examples and demos, you can think of these as just two <div> elements next to one another.

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

CSS Anchor Positioning is all about elements with absolute positioning (i.e., display: absolute), so there are also some concepts we have to review before diving in.

  • Containing Block: This is the box that contains the elements. For an absolute element, the containing block is the viewport the closest ancestor with a position other than static or certain values in properties like contain or filter.
  • Inset-Modified Containing Block (IMCB): For an absolute element, inset properties (top, right, bottom, left, etc.) reduce the size of the containing block into which it is sized and positioned, resulting in a new box called the inset-modified containing block, or IMCB for short. This is a vital concept to know since properties we’re covering in this guide — like position-area and position-try-order — rely on this concept.

Attaching targets to anchors

We’ll first look at the two properties that establish anchor positioning. The first, anchor-name, establishes the anchor element, while the second, position-anchor, attaches a target element to the anchor element.

Square labeled as "anchor"

anchor-name

A normal element isn’t an anchor by default — we have to explicitly make an element an anchor. The most common way is by giving it a name, which we can do with the anchor-name property.

anchor-name: none | <dashed-ident>#

The name must be a <dashed-ident>, that is, a custom name prefixed with two dashes (--), like --my-anchor or --MyAnchor.

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

This gives us an anchor element. All it needs is something anchored to it. That’s what we call the “target” element which is set with the position-anchor property.

Square labeled as "target"

position-anchor

The target element is an element with an absolute position linked to an anchor element matching what’s declared on the anchor-name property. This attaches the target element to the anchor element.

position-anchor: auto | <anchor-element>

It takes a valid <anchor-element>. So, if we establish another element as the “anchor” we can set the target with the position-anchor property:

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

Normally, if a valid anchor element isn’t found, then other anchor properties and functions will be ignored.

Positioning targets

Now that we know how to establish an anchor-target relationship, we can work on positioning the target element in relation to the anchor element. The following two properties are used to set which side of the anchor element the target is positioned on (position-area) and conditions for hiding the target element when it runs out of room (position-visibility).

Anchor element with target elements spanning around it.

position-area

The next step is positioning our target relative to its anchor. The easiest way is to use the position-area property, which creates an imaginary 3×3 grid around the anchor element and lets us place the target in one or more regions of the grid.

position-area: auto | <position-area>

It works by setting the row and column of the grid using logical values like start and end (dependent on the writing mode); physical values like topleftrightbottom and the center shared value, then it will shrink the target’s IMCB into the region of the grid we chose.

.target {
  position-area: top right;
  /* or */
  position-area: start end;
}

Logical values refer to the containing block’s writing mode, but if we want to position our target relative to its writing mode we would prefix it with the self value.

.target {
  position-area: self-start self-end;
}

There is also the center value that can be used in every axis.

.target {
  position-area: center right;
  /* or */
  position-area: start center;
}

To place a target across two adjacent grid regions, we can use the prefix span- on any value (that isn’t center) a row or column at a time.

.target {
  position-area: span-top left;
  /* or */
  position-area: span-start start;
}

Finally, we can span a target across three adjacent grid regions using the span-all value.

.target {
  position-area: bottom span-all;
  /* or */
  position-area: end span-all;
}

You may have noticed that the position-area property doesn’t have a strict order for physical values; writing position-area: top left is the same as position-area: left top, but the order is important for logical value since position-area: start end is completely opposite to position-area: end start.

We can make logical values interchangeable by prefixing them with the desired axis using y-, x-, inline- or block-.

.target {
  position-area: inline-end block-start;
  /* or */
  position-area: y-start x-end;
}
Examples on each position-visibility value: always showing the target, anchors-visible hiding it when the anchor goes out of screen and no-overflow hiding it when the target overflows

position-visibility

It provides certain conditions to hide the target from the viewport.

position-visibility: always | anchors-visible | no-overflow
  • always: The target is always displayed without regard for its anchors or its overflowing status.
  • no-overflow: If even after applying the position fallbacks, the target element is still overflowing its containing block, then it is strongly hidden.
  • anchors-visible: If the anchor (not the target) has completely overflowed its containing block or is completely covered by other elements, then the target is strongly hidden.
position-visibility: always | anchors-visible | no-overflow

Setting fallback positions

Once the target element is positioned against its anchor, we can give the target additional instructions that tell it what to do if it runs out of space. We’ve already looked at the position-visibility property as one way of doing that — we simply tell the element to hide. The following two properties, however, give us more control to re-position the target by trying other sides of the anchor (position-try-fallbacks) and the order in which it attempts to re-position itself (position-try-order).

The two properties can be declared together with the position-try shorthand property — we’ll touch on that after we look at the two constituent properties.

Examples on each try-tactic: flip-block flipping the target from the top to the bottom, flip-inline from left to right and flip-start from left to top (single value) and top right to left bottom (two values)

position-try-fallbacks

This property accepts a list of comma-separated position fallbacks that are tried whenever the target overflows out of space in its containing block. The property attempts to reposition itself using each fallback value until it finds a fit or runs out of options.

position-try-fallbacks: none | [ [<dashed-ident> || <try-tactic>] | <'inset-area'>  ]#
  • none: Leaves the target’s position options list empty.
  • <dashed-ident>: Adds to the options list a custom @position-try fallback with the given name. If there isn’t a matching @position-try, the value is ignored.
  • <try-tactic>: Creates an option list by flipping the target’s current position on one of three axes, each defined by a distinct keyword. They can also be combined to add up their effects.
    • The flip-block keyword swaps the values in the block axis.
    • The flip-inline keyword swaps the values in the inline axis.
    • The flip-start keyword swaps the values diagonally.
  • <dashed-ident> || <try-tactic>: Combines a custom @try-option and a <try-tactic> to create a single-position fallback. The <try-tactic> keywords can also be combined to sum up their effects.
  • <"position-area"> Uses the position-area syntax to move the anchor to a new position.
.target {
  position-try-fallbacks:
    --my-custom-position,
    --my-custom-position flip-inline,
    bottom left;
}
two targets sorrounding an anchor, positioned where the IMCB is the largest.

position-try-order

This property chooses a new position from the fallback values defined in the position-try-fallbacks property based on which position gives the target the most space. The rest of the options are reordered with the largest available space coming first.

position-try-order: normal | most-width | most-height | most-block-size | most-inline-size

What exactly does “more space” mean? For each position fallback, it finds the IMCB size for the target. Then it chooses the value that gives the IMCB the widest or tallest size, depending on which option is selected:

  • most-width
  • most-height
  • most-block-size
  • most-inline-size
.target {
  position-try-fallbacks: --custom-position, flip-start;
  position-try-order: most-width;
}

position-try

This is a shorthand property that combines the position-try-fallbacks and position-try-order properties into a single declaration. It accepts first the order and then the list of possible position fallbacks.

position-try: < "position-try-order" >? < "position-try-fallbacks" >;

So, we can combine both properties into a single style rule:

.target {
  position-try: most-width --my-custom-position, flip-inline, bottom left;
}

Custom position and size fallbacks

@position-try

This at-rule defines a custom position fallback for the position-try-fallbacks property.

@position-try <dashed-ident> {
  <declaration-list>
}

It takes various properties for changing a target element’s position and size and grouping them as a new position fallback for the element to try.

Imagine a scenario where you’ve established an anchor-target relationship. You want to position the target element against the anchor’s top-right edge, which is easy enough using the position-area property we saw earlier:

.target {
  position: absolute;
  position-area: top right;
  width: 100px;
}

See how the .target is sized at 100px? Maybe it runs out of room on some screens and is no longer able to be displayed at anchor’s the top-right edge. We can supply the .target with the fallbacks we looked at earlier so that it attempts to re-position itself on an edge with more space:

.target {
  position: absolute;
  position-area: top right;
  position-try-fallbacks: top left;
  position-try-order: most-width;
  width: 100px;
}

And since we’re being good CSSer’s who strive for clean code, we may as well combine those two properties with the position-try shorthand property:

.target {
  position: absolute;
  position-area: top right;
  position-try: most-width, flip-inline, bottom left;
  width: 100px;
}

So far, so good. We have an anchored target element that starts at the top-right corner of the anchor at 100px. If it runs out of space there, it will look at the position-try property and decide whether to reposition the target to the anchor’s top-left corner (declared as flip-inline) or the anchor’s bottom-left corner — whichever offers the most width.

But what if we want to simulataneously re-size the target element when it is re-positioned? Maybe the target is simply too dang big to display at 100px at either fallback position and we need it to be 50px instead. We can use the @position-try to do exactly that:

@position-try --my-custom-position {
  position-area: top left;
  width: 50px;
}

With that done, we now have a custom property called --my-custom-position that we can use on the position-try shorthand property. In this case, @position-try can replace the flip-inline value since it is the equivalent of top left:

@position-try --my-custom-position {
  position-area: top left;
  width: 50px;
}

.target {
  position: absolute;
  position-area: top right;
  position-try: most-width, --my-custom-position, bottom left;
  width: 100px;
}

This way, the .target element’s width is re-sized from 100px to 50px when it attempts to re-position itself to the anchor’s top-right edge. That’s a nice bit of flexibility that gives us a better chance to make things fit together in any layout.

Anchor functions

anchor()

You might think of the CSS anchor() function as a shortcut for attaching a target element to an anchor element — specify the anchor, the side we want to attach to, and how large we want the target to be in one fell swoop. But, as we’ll see, the function also opens up the possibility of attaching one target element to multiple anchor elements.

This is the function’s formal syntax, which takes up to three arguments:

anchor( <anchor-element>? && <anchor-side>, <length-percentage>? )

So, we’re identifying an anchor element, saying which side we want the target to be positioned on, and how big we want it to be. It’s worth noting that anchor() can only be declared on inset-related properties (e.g. top, left, inset-block-end, etc.)

.target {
  top: anchor(--my-anchor bottom);
  left: anchor(--my-anchor end, 50%);
}

Let’s break down the function’s arguments.

<anchor-element>

This argument specifies which anchor element we want to attach the target to. We can supply it with either the anchor’s name (see “Attaching targets to anchors”).

We also have the choice of not supplying an anchor at all. In that case, the target element uses an implicit anchor element defined in position-anchor. If there isn’t an implicit anchor, the function resolves to its fallback. Otherwise, it is invalid and ignored.

<anchor-side>

This argument sets which side of the anchor we want to position the target element to, e.g. the anchor’s top, left, bottom, right, etc.

But we have more options than that, including logical side keywords (inside, outside), logical direction arguments relative to the user’s writing mode (start, end, self-start, self-end) and, of course, center.

  • <anchor-side>: Resolves to the <length> of the corresponding side of the anchor element. It has physical arguments (top, left, bottom right), logical side arguments (inside, outside), logical direction arguments relative to the user’s writing mode (start, end, self-start, self-end) and the center argument.
  • <percentage>: Refers to the position between the start (0%) and end (100%). Values below 0% and above 100% are allowed.
<length-percentage>

This argument is totally optional, so you can leave it out if you’d like. Otherwise, use it as a way of re-sizing the target elemenrt whenever it doesn’t have a valid anchor or position. It positions the target to a fixed <length> or <percentage> relative to its containing block.

Let’s look at examples using different types of arguments because they all do something a little different.

Using physical arguments
targets sorrounding the anchor. each with a different position

Physical arguments (top, right, bottom, left) can be used to position the target regardless of the user’s writing mode. For example, we can position the right and bottom inset properties of the target at the anchor(top) and anchor(left) sides of the anchor, effectively positioning the target at the anchor’s top-left corner:

.target {
  bottom: anchor(top);
  right: anchor(left);
}

Using logical side keywords
targets sorrounding the anchor. each with a different position

Logical side arguments (i.e., insideoutside), are dependent on the inset property they are in. The inside argument will choose the same side as its inset property, while the outside argument will choose the opposite. For example:

.target {
  left: anchor(outside);
  /* is the same as */
  left: anchor(right);

  top: anchor(inside);
  /* is the same as */
  top: anchor(top);
}

Using logical directions

targets sorrounding the anchor. each with a different position

Logical direction arguments are dependent on two factors:

  1. The user’s writing mode: they can follow the writing mode of the containing block (start, end) or the target’s own writing mode (self-start, self-end).
  2. The inset property they are used in: they will choose the same axis of their inset property.

So for example, using physical inset properties in a left-to-right horizontal writing would look like this:

.target {
  left: anchor(start);
  /* is the same as */
  left: anchor(left);

  top: anchor(end);
  /* is the same as */
  top: anchor(bottom);
}

In a right-to-left writing mode, we’d do this:

.target {
  left: anchor(start);
  /* is the same as */
  left: anchor(right);

  top: anchor(end);
  /* is the same as */
  top: anchor(bottom);
}

That can quickly get confusing, so we should also use logical arguments with logical inset properties so the writing mode is respected in the first place:

.target {
  inset-inline-start: anchor(end);
  inset-block-start: anchor(end);
}

Using percentage values
targets sorrounding the anchor. each with a different position

Percentages can be used to position the target from any point between the start (0%) and end (100% ) sides. Since percentages are relative to the user writing mode, is preferable to use them with logical inset properties.

.target {
  inset-inline-start: anchor(100%);
  /* is the same as */
  inset-inline-start: anchor(end);

  inset-block-end: anchor(0%);
  /* is the same as */
  inset-block-end: anchor(start);
}

Values smaller than 0% and bigger than 100% are accepted, so -100% will move the target towards the start and 200% towards the end.

.target {
  inset-inline-start: anchor(200%);
  inset-block-end: anchor(-100%);
}

Using the center keyword
targets sorrounding the anchor. each with a different position

The center argument is equivalent to 50%. You could say that it’s “immune” to direction, so there is no problem if we use it with physical or logical inset properties.

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

  left: anchor(center);
  bottom: anchor(top);
}

anchor-size()

The anchor-size() function is unique in that it sizes the target element relative to the size of the anchor element. This can be super useful for ensuring a target scales in size with its anchor, particularly in responsive designs where elements tend to get shifted, re-sized, or obscured from overflowing a container.

The function takes an anchor’s side and resolves to its <length>, essentially returning the anchor’s width, height, inline-size or block-size.

anchor-size( [ <anchor-element> || <anchor-size> ]? , <length-percentage>? )
anchor with an anchor-size() function on each side

Here are the arguments that can be used in the anchor-size() function:

  • <anchor-size>: Refers to the side of the anchor element.
  • <length-percentage>: This optional argument can be used as a fallback whenever the target doesn’t have a valid anchor or size. It returns a fixed <length> or <percentage> relative to its containing block.

And we can declare the function on the target element’s width and height properties to size it with the anchor — or both at the same time!

.target {
  width: anchor-size(width, 20%); /* uses default anchor */`
  height: anchor-size(--other-anchor inline-size, 100px);
}

Multiple anchors

We learned about the anchor() function in the last section. One of the function’s quirks is that we can only declare it on inset-based properties, and all of the examples we saw show that. That might sound like a constraint of working with the function, but it’s actually what gives anchor() a superpower that anchor positioning properties don’t: we can declare it on more than one inset-based property at a time. As a result, we can set the function multiple anchors on the same target element!

Here’s one of the first examples of the anchor() function we looked at in the last section:

.target {
  top: anchor(--my-anchor bottom);
  left: anchor(--my-anchor end, 50%);
}

We’re declaring the same anchor element named --my-anchor on both the top and left inset properties. That doesn’t have to be the case. Instead, we can attach the target element to multiple anchor elements.

.anchor-1 { anchor-name: --anchor-1; }
.anchor-2 { anchor-name: --anchor-2; }
.anchor-3 { anchor-name: --anchor-3; }
.anchor-4 { anchor-name: --anchor-4; }

.target {
  position: absolute;
  inset-block-start: anchor(--anchor-1);
  inset-inline-end: anchor(--anchor-2);
  inset-block-end: anchor(--anchor-3);
  inset-inline-start: anchor(--anchor-4);
}

Or, perhaps more succintly:

.anchor-1 { anchor-name: --anchor-1; }
.anchor-2 { anchor-name: --anchor-2; }
.anchor-3 { anchor-name: --anchor-3; }
.anchor-4 { anchor-name: --anchor-4; }

.target {
  position: absolute;
  inset: anchor(--anchor-1) anchor(--anchor-2) anchor(--anchor-3) anchor(--anchor-4);
}

The following demo shows a target element attached to two <textarea> elements that are registered anchors. A <textarea> allows you to click and drag it to change its dimensions. The two of them are absolutely positioned in opposite corners of the page. If we attach the target to each anchor, we can create an effect where resizing the anchors stretches the target all over the place almost like a tug-o-war between the two anchors.

The demo is only supported in Chrome at the time we’re writing this guide, so let’s drop in a video so you can see how it works.

Accessibility

The most straightforward use case for anchor positioning is for making tooltips, info boxes, and popovers, but it can also be used for decorative stuff. That means anchor positioning doesn’t have to establish a semantic relationship between the anchor and target elements. You can probably spot the issue right away: non-visual devices, like screen readers, are left in the dark about how to interpret two seemingly unrelated elements.

As an example, let’s say we have an element called .tooltip that we’ve set up as a target element anchored to another element called .anchor.

<div class="anchor">anchor</div>
<div class="toolip">toolip</div>
.anchor {
  anchor-name: --my-anchor;
}

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

We need to set up a connection between the two elements in the DOM so that they share a context that assistive technologies can interpret and understand. The general rule of thumb for using ARIA attributes to describe elements is generally: don’t do it. Or at least avoid doing it unless you have no other semantic way of doing it.

This is one of those cases where it makes sense to reach for ARIA atributes. Before we do anything else, a screen reader currently sees the two elements next to one another without any remarking 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! If you’re new to ARIA attributes, you ought to check out Adam Silver’s “Why, How, and When to Use Semantic HTML and ARIA” for a great introduction.

Browser support

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
125NoNo125No

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
129No129No

Spec changes

CSS Anchor Positioning has undergone several changes since it was introduced as an Editor’s Draft. The Chrome browser team was quick to hop on board and implement anchor positioning even though the feature was still being defined. That’s caused confusion because Chromium-based browsers implemented some pieces of anchor positioning while the specification was being actively edited.

We are going to outline specific cases for you where browsers had to update their implementations in response to spec changes. It’s a bit confusing, but as of Chrome 129+, this is the stuff that was shipped but changed:

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 to prevent us from having to do all that mental juggling:

.target {
  left: anchor(center);
}

Known bugs

Yes, there are some bugs with CSS Anchor Positioning, at least at the time this guide is being written. For example, the specification says that if an element doesn’t have a default anchor element, then the position-area does nothing. This is a known issue (#10500), but it’s still possible to replicate.

So, the following code…

.container {
  position: relative;
}

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

…will center the .element inside its container, at least in Chrome:

Credit to Afif13 for that great demo!

Another example involves the position-visibility property. If your anchor element is out of sight or off-screen, you typically want the target element to be hidden as well. The specification says that property’s the default value is anchors-visible, but browsers default to always instead.

The current implemenation in Chrome isn’t reflecting the spec; it indeed is using always as the initial value. But the spec is intentional: if your anchor is off-screen or otherwise scrolled off, you usually want it to hide. (#10425)

Almanac references

Anchor position properties

Anchor position functions

Anchor position at-rules

Further reading


CSS Anchor Positioning Guide 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.