https://developer.chrome.com/blog/tether-elements-to-each-other-with-css-anchor-positioning/
Skip to content
Home
Docs
Blog
Articles
Home
Docs
Blog
Articles
Blog
Photo of two small gold-plated anchors resting on top of each other
on a table
Tether elements to each other with CSS anchor positioning
Published on Wednesday, March 1, 2023
Jhey Tompkins
Jhey Tompkins
Whimsy Dev Supreme! Developer Relations Engineer @ Google
Website Twitter GitHub Mastodon
Table of contents
* Browser support
* The problem
* Current solutions
* Using anchor positioning
* Tracking scroll position
* Position fallback and auto positioning
* Examples
+ Context menus
+ Focus and follow
+ Bar chart calc
+ Resize Handles
+ A SelectMenu?
+ That's it!
How do you currently tether one element to another? You might try
tracking their positions, or use some form of wrapper element.
/* styles.css */
.container {
position: relative;
}
.anchored {
position: absolute;
}
These solutions often aren't ideal. They need JavaScript or introduce
extra markup. The CSS anchor positioning API aims to solve this by
providing a CSS API for tethering elements. It provides a means to
position and size one element based on the position and size of other
elements.
Image shows a mockup browser window detailing the anatomy of a
tooltip.
# Browser support
You can try out the CSS anchor positioning API in Chrome Canary
behind the "Experimental Web Platform Features" flag. To enable that
flag, open Chrome Canary and visit chrome://flags. Then enable the
"Experimental web platform features" flag.
There is also a polyfill in development by the team at Oddbird. Be
sure to check the repo at github.com/oddbird/css-anchor-positioning.
You can check for anchoring support with:
@supports(anchor-name: --foo) {
/* Styles... */
}
Note that this API is still in an experimental stage and could
change. This article covers the important parts at a high level. The
current implementation also isn't completely in sync with the CSS
Working Group spec.
# The problem
Why would you need to do this? A prominent use case would be creating
tooltips or tooltip-like experiences. In that case, you often want to
tether the tooltip to the content that it references. There is often
a need for some way to tether an element to another. You also expect
that interacting with the page doesn't break that tether--for example,
if a user scrolls or resizes the UI.
Another part of the problem is if you want to make sure the tethered
element remains in view--for example, if you open a tooltip and it
becomes clipped by the viewport bounds. This might not be a great
experience for users. You'd like the tooltip to adapt.
# Current solutions
Currently, there are some different ways you could approach the
issue.
First up is the rudimentary "Wrap the anchor" approach. You take both
elements and wrap them in a container. Then you can use position to
position the tooltip relative to the anchor.
.containing-block {
position: relative;
}
.tooltip {
position: absolute;
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
}
You can move the container and everything will stay where you want it
for the most part.
Another approach might be if you know the position of your anchor or
you can somehow track it. You could pass it to your tooltip with
custom properties.
Anchor me!
The anchor
:root {
--anchor-width: 120px;
--anchor-top: 40vh;
--anchor-left: 20vmin;
}
.anchor {
position: absolute;
top: var(--anchor-top);
left: var(--anchor-left);
width: var(--anchor-width);
}
.tooltip {
position: absolute;
top: calc(var(--anchor-top));
left: calc((var(--anchor-width) * 0.5) + var(--anchor-left));
transform: translate(-50%, calc(-100% - 10px));
}
But, what if you don't know the position of your anchor? You likely
need to intervene with JavaScript. You could do something like the
following code does, but now this means your styles are starting to
leak out of CSS and into JavaScript.
const setAnchorPosition = (anchored, anchor) => {
const bounds = anchor.getBoundingClientRect().toJSON();
for (const [key, value] of Object.entries(bounds)) {
anchored.style.setProperty(`--${key}`, value);
}
};
const update = () => {
setAnchorPosition(
document.querySelector('.tooltip'),
document.querySelector('.anchor')
);
};
window.addEventListener('resize', update);
document.addEventListener('DOMContentLoaded', update);
This starts to pose some questions:
* When do I calculate the styles?
* How do I calculate the styles?
* How often do I calculate the styles?
Does that solve it? It might for your use case, but there's one
issue: our solution doesn't adapt. It isn't responsive. What if my
anchored element gets cut off by the viewport?
Now you need to decide whether to react to this and how. The number
of questions and decisions you need to make is starting to grow. All
you want to do is anchor one element to another. And in an ideal
world, your solution will adjust and react to its surroundings.
The decisions you make will have to take CSS behavior into account
too, such as how containing blocks behave. This is something the
Popover API and top layer access will help with.
To ease some of that pain, you might reach for a JavaScript solution
to help you out. That will incur the cost of adding a dependency to
your project, and it could introduce performance issues depending on
how you use them. For example, some packages use
requestAnimationFrame to keep the position correct. This means you
and your team need to get familiar with the package and its
configuration options. As a result, your questions and decisions may
not get reduced, but changed instead. This is part of the "why" for
CSS anchor positioning. It will abstract you away from thinking about
performance issues when calculating position.
Here's what the code could look like for using "floating-ui", a
popular package for this problem:
import {computePosition, flip, offset, autoUpdate} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.1/+esm';
const anchor = document.querySelector('.anchor')
const tooltip = document.querySelector('.tooltip')
const updatePosition = () => {
computePosition(anchor, tooltip, {
placement: 'top',
middleware: [offset(10), flip()]
})
.then(({x, y}) => {
Object.assign(tooltip.style, {
left: `${x}px`,
top: `${y}px`
})
})
};
const clean = autoUpdate(anchor, tooltip, updatePosition);
Try re-positioning the anchor in this demo that uses that code.
The "tooltip" might not behave how you expect. It reacts to going
outside of the viewport on the y-axis but not the x-axis. Dig into
the documentation, and you'll likely find a solution that works for
you.
But, finding a package that works for your project can take a lot of
time. It's extra decisions and can be frustrating if it doesn't quite
work how you want.
# Using anchor positioning
Enter the CSS anchor positioning API. The idea is to keep your styles
in your CSS and reduce the number of decisions you need to make.
You're hoping to achieve the same result, but the goal is to make the
developer experience better.
* No JavaScript required.
* Let the browser work out the best position from your guidance.
* No more third party dependencies
* No wrapper elements.
* Works with elements that are in the top layer.
Let's recreate and tackle the problem we were trying to solve above.
But, instead, use the analogy of a boat with an anchor. These
represent the anchored element and anchor. The water represents the
containing block.
First, you need to choose how to define the anchor. You can do this
within your CSS by setting the anchor-name property on the anchor
element. It accepts a dashed-ident value.
.anchor {
anchor-name: --my-anchor;
}
Alternatively, you will be able to define an anchor in your HTML with
the anchor attribute. The attribute value is the ID of the anchor
element. This creates an implicit anchor.
I'm a boat!
Once you've defined an anchor, you can use the anchor function. The
anchor function takes 3 arguments:
* Anchor element: The anchor-name of the anchor to use--or, you can
omit the value to use an implicit anchor. It can be defined via
the HTML relationship, or with an anchor-default property with an
anchor-name value.
* Anchor side: A keyword of the position you want to use. This
could be top, right, bottom, left, center, etc. Or, you can pass
a percentage. For example, 50% would be equal to center.
* Fallback: This is an optional fallback value that accepts a
length or percentage.
You use the anchor function as a value for the inset properties (top,
right, bottom, left, or their logical equivalents) of the anchored
element. You can also use the anchor function in calc:
.boat {
bottom: anchor(--my-anchor top);
left: calc(anchor(--my-anchor center) - (var(--boat-size) * 0.5));
}
/* alternative with anchor-default */
.boat {
anchor-default: -my-anchor;
bottom: anchor(top);
left: calc(anchor(center) - (var(--boat-size) * 0.5));
}
There is no center inset property so one option is to use calc if you
know the size of your anchored element. Why not use translate? You
could use this:
.boat {
anchor-default: -my-anchor;
bottom: anchor(top);
left: anchor(center);
translate: -50% 0;
}
But, the browser doesn't take into consideration transformed
positions for anchored elements. It'll become clear why this is
important when considering position fallbacks and auto positioning.
At the time of writing, defining anchor-name with the anchor
attribute isn't implemented for non-Popover elements. Here's a demo
that should work soon.
You may have noticed the use of the custom property --boat-size
above. But, if you want to base the anchored element size on that of
the anchor, you can also access that size. Instead of calculating it
yourself, you can use the anchor-size function. For example, to make
our boat four times the width of our anchor:
.boat {
width: calc(4 * anchor-size(--my-anchor width));
}
You also have access to the height too with anchor-size(--my-anchor
height). And you can use it to set the size of either axis or both.
What if you want to anchor to an element with absolute positioning?
The rule is that the elements can't be siblings. In that case, you
can wrap the anchor with a container that has relative positioning.
Then you can anchor to it.
I'm a boat!
Check out this demo where you can drag the anchor around and the boat
will follow.
# Tracking scroll position
In some cases, your anchor element might be within a scrolling
container. At the same time, your anchored element might be outside
that container. Because scrolling happens on a different thread from
layout, you need a way to track it. The anchor-scroll property can do
this. You set it on the anchored element and give it the value of the
anchor you want to track.
.boat { anchor-scroll: --my-anchor; }
Try this demo where you can switch anchor-scroll on and off with the
checkbox in the corner.
The analogy falls a little flat here though, as in an ideal world,
your boat and anchor are both in the water. Also, features such as
the Popover API promote being able to keep related elements close.
Anchor positioning will work with elements that are in the top layer
though. This is one of the major benefits behind the API: being able
to tether elements in different flows.
Consider this demo that has a scrolling container with anchors that
have tooltips. The tooltip elements that are popovers might not be
co-located with the anchors:
But, you'll notice how the popovers track their respective anchor
links. You can resize that scrolling container and the positions will
update for you.
An anchored element can't detect when a tracked anchor goes outside
the scroll window. But, it will still track the anchor position.
There is no current way to clip the anchored element when the anchor
element goes out of view.
Warning
Features such as anchor-size and anchor-scroll are still in
development. They could change based on your input from trying out
the API.
# Position fallback and auto positioning
This is where anchor positioning power goes up a level. A
position-fallback can position your anchored element based on a set
of fallbacks you provide. You guide the browser with your styles and
let it work out the position for you.
The common use case here is a tooltip that should flip between
getting shown above or below an anchor. And this behavior is based on
whether the tooltip would get clipped by its container. That
container is usually the viewport.
If you dug into the code of the last demo, you'd have seen there was
a position-fallback property in use. If you scrolled the container
you may have noticed those anchored popovers jumped. This happened
when their respective anchors neared the viewport boundary. At that
moment, the popovers are trying to adjust to stay in the viewport.
Before creating an explicit position-fallback, anchor positioning
will also offer automatic positioning. You can get that flip for free
by using a value of auto in both the anchor function and the opposite
inset property. For example, if you use anchor for bottom, set top to
auto.
.tooltip {
position: absolute;
bottom: anchor(--my-anchor auto);
top: auto;
}
This demo uses auto positioning syntax. At the time of writing this
isn't implemented. Be sure to check out the spec for the rules around
automatic positioning.
The alternative to auto positioning is to use an explicit
position-fallback. This requires you to define a position fallback
set. The browser will go through these until it finds one it can use
and then apply that positioning. If it can't find one that works, it
defaults to the first one defined.
A position-fallback that tries to display the tooltips above then
below could look like this:
@position-fallback --top-to-bottom {
@try {
bottom: anchor(top);
left: anchor(center);
}
@try {
top: anchor(bottom);
left: anchor(center);
}
}
Applying that to the tooltips looks like this:
.tooltip {
anchor-default: --my-anchor;
position-fallback: --top-to-bottom;
}
The use of anchor-default means you can reuse the position-fallback
for other elements. You could also use a scoped custom property to
set anchor-default.
Be mindful that some user agent styles may set inset properties for
you. In these cases, you may want to unset these before defining a
position-fallback. One example is elements using the popover
attribute. Applying inset: unset; will work.
Consider this demo using the boat again. There is a position-fallback
set. As you change the position of the anchor, the boat will adjust
to remain within the container. Try changing the padding value too
which adjusts the body padding. Notice how the browser corrects
positioning. The positions are being changed by changing the grid
alignment of the container.
The position-fallback is more verbose this time trying positions in a
clockwise direction.
.boat {
anchor-default: --my-anchor;
position-fallback: --compass;
}
@position-fallback --compass {
@try {
bottom: anchor(top);
right: anchor(left);
}
@try {
bottom: anchor(top);
left: anchor(right);
}
@try {
top: anchor(bottom);
right: anchor(left);
}
@try {
top: anchor(bottom);
left: anchor(right);
}
}
---------------------------------------------------------------------
# Examples
Now you have an idea of the main features for anchor positioning,
let's take a look at some interesting examples beyond tooltips. These
examples aim to get your ideas flowing for ways in which you could
use anchor positioning. The best way to take the spec further is with
input from real users like you.
# Context menus
Let's start with a context menu using the Popover API. The idea is
that clicking the button with the chevron will reveal a context menu.
And that menu will have its own menu to expand.
The markup isn't the important part here. But, you have three buttons
each using popovertarget. Then you have three elements using the
popover attribute. That gives you the ability to open the context
menus without any JavaScript. That could look like this:
...
...
The nested context menus won't close the parent one because they're
ancestral popovers.
Now, you can define a position-fallback and share it between the
context menus. We make sure to unset any inset styles for the
popovers too.
[popovertarget="share"] {
anchor-name: --share;
}
[popovertarget="playlist"] {
anchor-name: --playlist;
}
[popovertarget="context"] {
anchor-name: --context;
}
#share {
anchor-default: --share;
position-fallback: --aligned;
}
#playlist {
anchor-default: --playlist;
position-fallback: --aligned;
}
#context {
anchor-default: --context;
position-fallback: --flip;
}
@position-fallback --aligned {
@try {
top: anchor(top);
left: anchor(right);
}
@try {
top: anchor(bottom);
left: anchor(right);
}
@try {
top: anchor(top);
right: anchor(left);
}
@try {
bottom: anchor(bottom);
left: anchor(right);
}
@try {
right: anchor(left);
bottom: anchor(bottom);
}
}
@position-fallback --flip {
@try {
bottom: anchor(top);
left: anchor(left);
}
@try {
right: anchor(right);
bottom: anchor(top);
}
@try {
top: anchor(bottom);
left: anchor(left);
}
@try {
top: anchor(bottom);
right: anchor(right);
}
}
This gives you an adaptive nested context menu UI. Try changing the
content position with the select. The option you choose updates grid
alignment. And that affects how anchor positioning positions the
popovers.
# Focus and follow
This demo combines CSS primitives by bringing in :has(). The idea is
to transition a visual indicator for the input that has focus.
Do this by setting a new anchor at runtime. For this demo, a scoped
custom property gets updated on input focus.
#email {
anchor-name: --email;
}
#name {
anchor-name: --name;
}
#password {
anchor-name: --password;
}
:root:has(#email:focus) {
--active-anchor: --email;
}
:root:has(#name:focus) {
--active-anchor: --name;
}
:root:has(#password:focus) {
--active-anchor: --password;
}
:root {
--active-anchor: --name;
--active-left: anchor(var(--active-anchor) right);
--active-top: calc(
anchor(var(--active-anchor) top) +
(
(
anchor(var(--active-anchor) bottom) -
anchor(var(--active-anchor) top)
) * 0.5
)
);
}
.form-indicator {
left: var(--active-left);
top: var(--active-top);
transition: all 0.2s;
}
But, how might you take this further? You could use it for some form
of instructional overlay. A tooltip could move between points of
interest and update its content. You could crossfade the content.
Discrete animations allowing you to animate display or View
Transitions could work here.
# Bar chart calc
Another fun thing you can do with anchor positioning is combine it
with calc. Imagine a chart where you have some popovers that annotate
the chart.
You could track the highest and lowest values using CSS min and max.
The CSS for that could look something like this:
.chart__tooltip--max {
left: anchor(--chart right);
bottom: max(
anchor(--anchor-1 top),
anchor(--anchor-2 top),
anchor(--anchor-3 top)
);
translate: 0 50%;
}
There is some JavaScript at play to update the chart values and some
CSS to style the chart. But anchor positioning takes care of the
layout updates for us.
# Resize Handles
You don't have to only anchor to one element. You could use many
anchors for an element. You might have noticed that in the bar chart
example. The tooltips were anchored to the chart and then the
appropriate bar. If you took that concept a little further you could
use it to resize elements.
You could treat the anchor points like custom resize handles and lean
into an inset value.
.container {
position: absolute;
inset:
anchor(--handle-1 top)
anchor(--handle-2 right)
anchor(--handle-2 bottom)
anchor(--handle-1 left);
}
In this demo, GreenSock Draggable makes the handles Draggable. But,
the element resizes to fill the container that adjusts to fill
the gap between the handles.
# A SelectMenu?
This last one's a bit of a tease for what's to come. But, you can
create a focusable popover and now you have anchor positioning. You
could create the foundations of a styleable