Lesson 23: Anchor Positioning
When you’re placing a tooltip or a drop-down menu, you often want to position it relative to another element on the page. There have always been ways to do this with absolute positioning. But more complex needs have usually meant positioning items with JavaScript.
CSS anchor positioning gives you a way to position an element relative to another element, right in your CSS.
Tethering elements
To make an element an anchor, you give it an anchor-name value.
This is any string that starts with two dashes.
This is the identifier that the positioned element will use to find its anchor, so it helps to give it a descriptive name.
You can even give an element more than one anchor name, if it will be used as an anchor in different ways.
You need to set a few properties on the positioned element so it can be tethered.
First, you need to pull the element out of the document’s flow, so that it floats.
You do this by setting position: absolute or position: fixed.
Next, you need to set which anchor you want to tether to.
You do this by setting position-anchor to the anchor name you set on the anchor.
Finally, you need to set how to position the anchor.
You will learn more about position-area later in this module.
#anchor {
anchor-name: --my-anchor;
}
#positionedElement {
position: absolute;
position-anchor: --my-anchor;
position-area: end;
}Implicit tethers
Popovers are even simpler to tether.
When you open a popover using a button with a popovertarget, or by setting a source with showPopover({source}), the popover has an implicit anchor already set.
A popover is already floating with position: fixed by default.
So to position a popover, all you need to do is set the position.
#anchor{}
#positionedElement {
position-area: end;
margin: unset;
}[!NOTE] You also need to
unsetthe popover’s margin. By default, a popover’s margin isauto, which affects its positioning.
Scoping the potential anchors
You may use anchor positioning as part of a component, so that you can reuse a pattern like a drop-down menu in many places.
If you are using the same anchor-name more than once, how do you make sure that each positioned element finds the right anchor?
JavaScript solutions add a unique ID to each anchor, and then refer to that from the positioned element.
This gets cumbersome.
CSS has a simpler solution with anchor-scope.
The anchor-scope property sets which anchor names will be matched only among an element and its descendants.
It accepts a list of one or more anchor names, or the keyword all to limit the scope of all defined anchor names.
You ideally add an anchor-scope to an ancestor of both the positioned element and the anchor element that doesn’t contain other anchor elements with the same name.
Often, this is on the reusable component’s root.
The following example shows the difference anchor-scope makes when applied to repeated elements with the same anchor-name.
In the example, all of the <img> elements and the image banners reference the --image anchor name.
When anchor-scope is applied to the <li> elements, position-anchor: --image will match only the <img> element within the same <li> element as the banner.
Otherwise it will match the last rendered <img>.
[!NOTE] The way an anchor is found is complex, but the key point is that the anchor has to be fully laid out before the positioned element. This means you can’t tether to an element in a higher layer, like in a popover. You also can’t tether to absolute positioned elements that come after the positioned element. If there are several anchors with the same anchor name, the winner is the last one in the document that is already laid out.
Positioning
Now that you’ve tethered the element to your anchor, it’s time to position it.
Anchor positioning gives you two ways to position.
You can use position-area and the anchor() function.
position-area
The position-area property lets you position an element around the anchor by giving one or two keywords.
This covers many common cases, and it’s often a good place to start.
How position-area works
position-area works by creating a new containing block for the positioned element.
This block sits in an area made by the edges of the anchor and the positioned element’s original containing block.
[!NOTE] Because
position-areacreates a new containing block for the positioned element, percentages work against that new block. So if you use percentages forpaddingormargin, they will be a percentage of the new containing block.
There are many keywords for position-area.
You can break them down into a few groups to make them easier to understand.
Anchor-tool.com is a great tool for exploring the syntax.
Physical keywords
You can use the physical keywords, top, left, bottom, right, and center.
For example, position-area: top right will place the positioned element above and to the right of the anchor.
These keywords also have physical axis equivalents, y-start, x-start, y-end, and x-end.
Logical keywords
You can also use logical keywords, block-start, block-end, inline-start, and inline-end.
For example, position-area: block-end inline-start will place the positioned element beneath and to the left of the anchor in languages like English.
In other writing modes, it places the element after the anchor on the block axis and before the anchor on the inline axis.
You can also use center with a logical keyword.
You can also leave out the axis if you are using logical keywords, with the block axis first and the inline axis second.
position-area: start end is the same as position-area: block-start inline-end or even position-area: inline-end block-start.
[!NOTE] Logical properties position an element based on the writing mode of its containing block. If you want to use the positioned element’s own writing mode, you can add the
selfmodifier. For example, useself-block-startinstead ofblock-start.
Spanning multiple grid areas
So far, you may have noticed that these options only let you place the positioned element within a single grid space.
Adding the span prefix to physical or logical properties adds the next-door center grid space.
position-area: span-top right will be positioned to the right of the anchor, and from the anchor’s bottom to the top of the positioned element’s original containing block.
A common position-area for a drop-down menu is position-area: block-end span-inline-end.
The span-all keyword spans 3 rows or columns.
Single keyword
If you only set one keyword, the other axis is set for you. This mostly works the way you would expect, but it helps to understand how it works.
If the keyword you give is clear about its axis, the other axis is computed as span-all.
This means that position-area: bottom is the same as position-area: bottom span-all.
The positioned element will be below the anchor, and have the entire width of the containing block available.
If the keyword doesn’t clearly point to an axis, it’s repeated.
position-area: start is the same as start start, and is placed at the top left of the anchor in left to right languages.
The anchor() function
For more advanced cases, position-area may not be enough.
The anchor() function lets you set individual inset properties based on the position of another element.
This resolves to a CSS length, which means you can use it in calculations and with other CSS functions.
You can also tether different sides to different anchors.
The anchor() function takes an anchor name and an anchor side.
If your element has a default anchor, set with position-anchor or set implicitly, for example with a popover, you can leave out the anchor name.
.positionedElement {
block-start: anchor(--my-anchor start);
/* OR */
position-anchor: --my-anchor;
block-start: anchor(start);
}Fallback values
If an anchor can’t be found for an anchor() function, the whole declaration becomes invalid.
This might happen if the anchor is rendered after the positioned element, or if there isn’t an element with a matching anchor-name.
To handle this, you can set a fallback length or percentage.
.positionedElement {
block-start: anchor(--my-anchor, 100px)
}In the preceding example, the positioned element’s left value is anchored to --focused-anchor.
But that anchor-name only exists when the first button is hovered or focused.
Because an anchor() function resolves to a length, you can use another anchor as a fallback.
If we didn’t provide a fallback, the positioned element wouldn’t be positioned.
Anchor side keywords
The anchor side value chooses which of the anchor’s edges to position against.
Like position-area, the anchor side value supports several different types of syntax.
| Type | Values | Description |
|---|---|---|
| Physical | top, left, bottom, right | Physical keywords correspond to a specific side of the anchor, but can only be used on the same axis as the positioned element’s inset that you are setting. For example, top: anchor(bottom) positions the element’s top at the anchor’s bottom, but left: anchor(top) won’t work. |
| Side | inside, outside | The inside keyword corresponds to the same side as the inset property, and the outside keyword corresponds to the opposite side on the same axis. For example, inset-block-start: anchor(inside) refers to the block-start side of the anchor, and inset-inline-end: (outside) refers to the inline-start side of the anchor. |
| Logical | start, end, self-start, self-end | Logical keywords refer to the anchor’s sides based on the writing mode of the positioned element with self-start and self-end, or with the writing mode of the positioned element’s containing block with start and end. |
| Percentage | 0% - 100% | A percentage value places the positioned element along the axis from the anchor’s start to end on the specified axis. 0% is at the anchor’s start side, and 100% is the anchor’s end side. center is equivalent to 50%. If you are using a percentage on an end-side inset like bottom, this is not reversed, so 0% is still the anchor’s start side. |
This example shows how a percentage value always goes from the start to the end on the specified axis.
Using anchor()
Because anchor() is a length, it’s very flexible.
You can change the value with CSS functions like max() and calc().
One limit is that you can only use anchor() functions on inset properties.
The preceding example adds a background behind the open details panel that animates smoothly when a different panel is opened, and stretches to include a hovered details panel.
To do this, it uses min() to pick the smaller length between two anchors.
#indicator{
/* Use the smaller of the 2 values: */
inset-block-start: min(
/* 1. The start side of the default anchor, which is the open `<details>` element */
anchor(start),
/* 2. The start side of the hovered `<details>` element. */
anchor(--hovered start,
/* If no `<details>` element is hovered, this falls back to infinity px, so that the other value is smaller, and therefore used. */
var(calc(1px * infinity)))
);
}The example also uses calc() to add inline space around the open panel.
Using the anchor’s size
You can also use the anchor-size() function to use the anchor’s dimensions for your positioned element’s size, position, or margin.
anchor-size() takes an anchor name, or uses the default anchor.
By default, it will use the size of the anchor on the axis where it’s being used.
So width: anchor-size() will return the anchor’s width.
You can also use the other axis by saying which length you want.
You do this with the physical keywords width and height, or the logical keywords block, inline, self-block, and self-inline.
Handling overflow
You’ve made a drop-down menu component, and used anchor positioning to place the drop-down menu where you want it to go. But then you move the menu over to the other side of the screen, or use it for a user menu, and the user’s name is extra long. Suddenly, your drop-down is off the screen. Now what?
CSS anchor positioning has a built-in system that lets you quickly build a robust set of fallbacks for when your positioned element ends up outside of its containing block.
Fallback options
The position-try-fallbacks rule takes a list of fallback options.
When the default position overflows, each option is tried in order until there is a position that doesn’t overflow.
You can use any position-area value as a fallback option.
In this example, in left to right writing modes like English, the positioned element will first try to sit at the bottom of the anchor, spanning the center and right columns.
If that overflows, it will try to sit at the bottom of the anchor, spanning the left and center columns.
If that overflows too, the position reverts back to the default position, even though that overflows.
.positioned-element {
position-area: block-end span-inline-end;
position-try-fallbacks: block-end span-inline-start;
}There are also several flip- keywords that handle common fallback cases.
flip-block and flip-inline try flipping the element over the block and inline axes.
They can also be combined with flip-block flip-inline to flip over both axes.
The flip-start value flips the positioned element over a diagonal line from the start to the end corners of the anchor.
You can also create a custom fallback option with @position-try.
This lets you set the margins, alignment, and even change the anchor.
@position-try --menu-below {
position-area: bottom span-right;
margin-top: 1em;
}
#positioned-element {
position-try: --menu-below;
}flip-block and flip-inline can be added to @position-try fallback options to create a variant.
#positioned-element {
position-try: --menu-below, flip-inline --menu-below;
}In the preceding example, the browser follows these steps, stopping as soon as it finds a solution that doesn’t overflow.
- The element is placed with
position-area: end, at the bottom right of the anchor. - If that overflows, the element is placed with the custom fallback option
named
--bottom-span-right, which places it withposition-area: bottom span-right, with an additional margin beneath. - If that overflows, the element is placed with
flip-inline --bottom-span-right, which combines the custom fallback option withflip-inline, which is essentiallyposition-area: bottom span-left. - If that overflows, the element is placed using the
--use-alternatecustom fallback option, which places it below a completely different anchor. - If that overflows, the element reverts to its original placement, with
position-area: end, even though that is known to overflow.
Fallback order
By default, when the first position overflows, the browser will try each option in position-try-fallbacks until it finds a position that doesn’t overflow.
You can override this with position-try-order to test each fallback option, and use the one that has the most space on a given axis.
You can set the axis with either the logical keywords, most-block-size and most-inline-size, or with the physical keywords most-height and most-width.
position-try-order and position-try-fallbacks can be combined with the position-try shorthand, with the order coming first.
Scrolling
When a user scrolls, they expect the page to move smoothly. To make this happen, browsers put limits on how anchor positioning can be used when scrolling.
You can tether a positioned element to anchors in different scroll containers.
But the element will only move in response to one of the anchors scrolling.
This will be the default anchor, which is either the implicit anchor from a popover, or the value of position-anchor.
You’ll notice that the positioned element stays visible even as the anchor is scrolled out of view.
To hide the positioned element when the anchor is hidden, set position-visibility: anchors-visible.
This applies when the anchor is overscrolled, and also if it’s hidden in other ways, for example with visibility: hidden.
Adapted from Learn CSS © Google and contributors, licensed under CC BY 4.0 (prose) and Apache 2.0 (code samples).