Tether components to one another with CSS anchor positioning
How do you at the moment tether one ingredient to a different? You would possibly strive monitoring their positions, or use some type of wrapper ingredient.
<div class="container">
<a href="/hyperlink" class="anchor">I’m the anchor</a>
<div class="anchored">I’m the anchored factor</div>
</div>
.container {
place: relative;
}
.anchored {
place: absolute;
}
These options typically aren’t perfect. They want JavaScript or introduce additional markup. The CSS anchor positioning API goals to resolve this by offering a CSS API for tethering components. It supplies a way to place and dimension one ingredient based mostly on the place and dimension of different components.
# Browser assist
You’ll be able to check out the CSS anchor positioning API in Chrome Canary behind the “Experimental Net Platform Options” flag. To allow that flag, open Chrome Canary and go to chrome://flags
. Then allow the “Experimental net platform options” flag.
There’s additionally a polyfill in development by the group at Oddbird. Remember to test the repo at github.com/oddbird/css-anchor-positioning.
You’ll be able to test for anchoring assist with:
@helps(anchor-name: --foo) {
}
Notice that this API remains to be in an experimental stage and will change. This text covers the essential components at a excessive stage. The present implementation additionally is not fully in sync with the CSS Working Group spec.
# The issue
Why would you could do that? A distinguished use case could be creating tooltips or tooltip-like experiences. In that case, you typically wish to tether the tooltip to the content material that it references. There’s typically a necessity for some solution to tether a component to a different. You additionally anticipate that interacting with the web page would not break that tether—for instance, if a person scrolls or resizes the UI.
One other a part of the issue is if you wish to ensure that the tethered ingredient stays in view—for instance, in the event you open a tooltip and it turns into clipped by the viewport bounds. This may not be a fantastic expertise for customers. You’d just like the tooltip to adapt.
# Present options
At present, there are some alternative ways you might method the problem.
First up is the rudimentary “Wrap the anchor” method. You’re taking each components and wrap them in a container. Then you need to use place
to place the tooltip relative to the anchor.
<div class="containing-block">
<div class="tooltip">Anchor me!</div>
<a class="anchor">The anchor</a>
</div>
.containing-block {
place: relative;
}.tooltip {
place: absolute;
backside: calc(100% + 10px);
left: 50%;
rework: translateX(-50%);
}
You’ll be able to transfer the container and every part will keep the place you need it for probably the most half.
One other method may be if you realize the place of your anchor or you may by some means observe it. You would cross it to your tooltip with customized properties.
<div class="tooltip">Anchor me!</div>
<a class="anchor">The anchor</a>
:root {
--anchor-width: 120px;
--anchor-top: 40vh;
--anchor-left: 20vmin;
}.anchor {
place: absolute;
high: var(--anchor-top);
left: var(--anchor-left);
width: var(--anchor-width);
}
.tooltip {
place: absolute;
high: calc(var(--anchor-top));
left: calc((var(--anchor-width) * 0.5) + var(--anchor-left));
rework: translate(-50%, calc(-100% - 10px));
}
However, what if you do not know the place of your anchor? You doubtless must intervene with JavaScript. You would do one thing like the next code does, however now this implies your kinds are beginning 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.model.setProperty(`--${key}`, worth);
}
};const replace = () => {
setAnchorPosition(
doc.querySelector('.tooltip'),
doc.querySelector('.anchor')
);
};
window.addEventListener('resize', replace);
doc.addEventListener('DOMContentLoaded', replace);
This begins to pose some questions:
- When do I calculate the kinds?
- How do I calculate the kinds?
- How typically do I calculate the kinds?
Does that resolve it? It would to your use case, however there’s one problem: our answer would not adapt. It is not responsive. What if my anchored ingredient will get lower off by the viewport?
Now you could resolve whether or not to react to this and the way. The variety of questions and choices you could make is beginning to develop. All you wish to do is anchor one ingredient to a different. And in a perfect world, your answer will alter and react to its environment.
The selections you make should take CSS habits into consideration too, similar to how containing blocks behave. That is one thing the Popover API and top layer entry will assist with.
To ease a few of that ache, you would possibly attain for a JavaScript answer that will help you out. That can incur the price of including a dependency to your undertaking, and it may introduce efficiency points relying on how you employ them. For instance, some packages use requestAnimationFrame
to maintain the place right. This implies you and your group must get conversant in the package deal and its configuration choices. Consequently, your questions and choices might not get lowered, however modified as an alternative. That is a part of the “why” for CSS anchor positioning. It should summary you away from excited about efficiency points when calculating place.
Here is what the code may appear to be for utilizing “floating-ui“, a preferred package deal for this downside:
import {computePosition, flip, offset, autoUpdate} from 'https://cdn.jsdelivr.web/npm/@floating-ui/dom@1.2.1/+esm';const anchor = doc.querySelector('.anchor')
const tooltip = doc.querySelector('.tooltip')
const updatePosition = () => {
computePosition(anchor, tooltip, {
placement: 'high',
middleware: [offset(10), flip()]
})
.then(({x, y}) => {
Object.assign(tooltip.model, {
left: `${x}px`,
high: `${y}px`
})
})
};
const clear = autoUpdate(anchor, tooltip, updatePosition);
Attempt re-positioning the anchor on this demo that makes use of that code.
The “tooltip” may not behave the way you anticipate. It reacts to going exterior of the viewport on the y-axis however not the x-axis. Dig into the documentation, and also you’ll doubtless discover a answer that works for you.
However, discovering a package deal that works to your undertaking can take a whole lot of time. It is additional choices and will be irritating if it would not fairly work the way you need.
# Utilizing anchor positioning
Enter the CSS anchor positioning API. The concept is to maintain your kinds in your CSS and cut back the variety of choices you could make. You are hoping to attain the identical outcome, however the aim is to make the developer expertise higher.
- No JavaScript required.
- Let the browser work out the very best place out of your steering.
- No extra third occasion dependencies
- No wrapper components.
- Works with components which are within the high layer.
Let’s recreate and sort out the issue we have been attempting to resolve above. However, as an alternative, use the analogy of a ship with an anchor. These characterize the anchored ingredient and anchor. The water represents the containing block.
First, you could select the right way to outline the anchor. You are able to do this inside your CSS by setting the anchor-name
property on the anchor ingredient. It accepts a dashed-ident worth.
.anchor {
anchor-name: --my-anchor;
}
Alternatively, it is possible for you to to outline an anchor in your HTML with the anchor
attribute. The attribute worth is the ID of the anchor ingredient. This creates an implicit anchor.
<a id="my-anchor" class="anchor"></a>
<div anchor="my-anchor" class="boat">I’m a ship!</div>
As soon as you have outlined an anchor, you need to use the anchor
perform. The anchor
perform takes 3 arguments:
- Anchor ingredient: The
anchor-name
of the anchor to make use of—or, you may omit the worth to make use of animplicit
anchor. It may be outlined by way of the HTML relationship, or with ananchor-default
property with ananchor-name
worth. - Anchor facet: A key phrase of the place you wish to use. This might be
high
,proper
,backside
,left
,heart
, and so on. Or, you may cross a share. For instance, 50% could be equal toheart
. - Fallback: That is an non-obligatory fallback worth that accepts a size or share.
You employ the anchor
perform as a worth for the inset properties (high
, proper
, backside
, left
, or their logical equivalents) of the anchored ingredient. It’s also possible to use the anchor
perform in calc
:
.boat {
backside: anchor(--my-anchor high);
left: calc(anchor(--my-anchor heart) - (var(--boat-size) * 0.5));
}
.boat {
anchor-default: –my-anchor;
backside: anchor(high);
left: calc(anchor(heart) - (var(--boat-size) * 0.5));
}
There is no such thing as a heart
inset property so one possibility is to make use of calc
if you realize the scale of your anchored ingredient. Why not use translate
? You would use this:
.boat {
anchor-default: –my-anchor;
backside: anchor(high);
left: anchor(heart);
translate: -50% 0;
}
However, the browser would not take into accounts remodeled positions for anchored components. It’s going to change into clear why that is essential when contemplating place fallbacks and auto positioning.
On the time of writing, defining anchor-name
with the anchor
attribute isn’t carried out for non-Popover components. Right here’s a demo that ought to work quickly.
You might have observed using the customized property --boat-size
above. However, if you wish to base the anchored ingredient dimension on that of the anchor, you may as well entry that dimension. As an alternative of calculating it your self, you need to use the anchor-size
perform. For instance, to make our boat 4 occasions the width of our anchor:
.boat {
width: calc(4 * anchor-size(--my-anchor width));
}
You even have entry to the peak too with anchor-size(--my-anchor peak)
. And you need to use it to set the scale of both axis or each.
What if you wish to anchor to a component with absolute
positioning? The rule is that the weather cannot be siblings. In that case, you may wrap the anchor with a container that has relative
positioning. Then you may anchor to it.
<div class="anchor-wrapper">
<a id="my-anchor" class="anchor"></a>
</div>
<div class="boat">I’m a ship!</div>
Try this demo the place you may drag the anchor round and the boat will comply with.
In some circumstances, your anchor ingredient may be inside a scrolling container. On the identical time, your anchored ingredient may be exterior that container. As a result of scrolling occurs on a distinct thread from format, you want a solution to observe it. The anchor-scroll
property can do that. You set it on the anchored ingredient and provides it the worth of the anchor you wish to observe.
.boat { anchor-scroll: --my-anchor; }
Do that demo the place you may swap anchor-scroll
on and off with the checkbox within the nook.
The analogy falls just a little flat right here although, as in a perfect world, your boat and anchor are each within the water. Additionally, options such because the Popover API promote with the ability to maintain associated components shut. Anchor positioning will work with components which are within the high layer although. This is among the main advantages behind the API: with the ability to tether components in several flows.
Think about this demo that has a scrolling container with anchors which have tooltips. The tooltip components which are popovers may not be co-located with the anchors:
However, you may discover how the popovers observe their respective anchor hyperlinks. You’ll be able to resize that scrolling container and the positions will replace for you.
An anchored ingredient cannot detect when a tracked anchor goes exterior the scroll window. However, it’s going to nonetheless observe the anchor place. There is no such thing as a present solution to clip the anchored ingredient when the anchor ingredient goes out of view.
Options similar to anchor-size
and anchor-scroll
are nonetheless in improvement. They may change based mostly in your enter from attempting out the API.
# Place fallback and auto positioning
That is the place anchor positioning energy goes up a stage. A position-fallback
can place your anchored ingredient based mostly on a set of fallbacks you present. You information the browser together with your kinds and let it work out the place for you.
The frequent use case here’s a tooltip that ought to flip between getting proven above or beneath an anchor. And this habits relies on whether or not the tooltip would get clipped by its container. That container is often the viewport.
In the event you dug into the code of the final demo, you’d have seen there was a position-fallback
property in use. In the event you scrolled the container you might have observed these anchored popovers jumped. This occurred when their respective anchors neared the viewport boundary. At that second, the popovers try to regulate to remain within the viewport.
Earlier than creating an specific position-fallback
, anchor positioning will even supply automatic positioning. You will get that flip totally free through the use of a worth of auto
in each the anchor perform and the other inset property. For instance, in the event you use anchor
for backside
, set high
to auto
.
.tooltip {
place: absolute;
backside: anchor(--my-anchor auto);
high: auto;
}
The choice to auto positioning is to make use of an specific position-fallback
. This requires you to outline a place fallback set. The browser will undergo these till it finds one it could possibly use after which apply that positioning. If it could possibly’t discover one which works, it defaults to the primary one outlined.
A position-fallback
that tries to show the tooltips above then beneath may appear to be this:
@position-fallback --top-to-bottom {
@strive {
backside: anchor(high);
left: anchor(heart);
}@strive {
high: anchor(backside);
left: anchor(heart);
}
}
Making use of that to the tooltips seems to be like this:
.tooltip {
anchor-default: --my-anchor;
position-fallback: --top-to-bottom;
}
The usage of anchor-default
means you may reuse the position-fallback
for different components. You would additionally use a scoped customized property to set anchor-default
.
Be conscious that some person agent kinds might set inset properties for you. In these circumstances, you could wish to unset these earlier than defining a position-fallback
. One instance is components utilizing the popover
attribute. Making use of inset: unset;
will work.
Think about this demo utilizing the boat once more. There’s a position-fallback
set. As you alter the place of the anchor, the boat will alter to stay inside the container. Attempt altering the padding worth too which adjusts the physique padding. Discover how the browser corrects positioning. The positions are being modified by altering the grid alignment of the container.
The position-fallback
is extra verbose this time attempting positions in a clockwise course.
.boat {
anchor-default: --my-anchor;
position-fallback: --compass;
}@position-fallback --compass {
@strive {
backside: anchor(high);
proper: anchor(left);
}
@strive {
backside: anchor(high);
left: anchor(proper);
}
@strive {
high: anchor(backside);
proper: anchor(left);
}
@strive {
high: anchor(backside);
left: anchor(proper);
}
}
# Examples
Now you will have an concept of the principle options for anchor positioning, let’s check out some attention-grabbing examples past tooltips. These examples intention to get your concepts flowing for methods wherein you might use anchor positioning. The easiest way to take the spec additional is with enter from actual customers such as you.
Let’s begin with a context menu utilizing the Popover API. The concept is that clicking the button with the chevron will reveal a context menu. And that menu can have its personal menu to develop.
The markup is not the essential half right here. However, you will have three buttons every utilizing popovertarget
. Then you will have three components utilizing the popover
attribute. That offers you the flexibility to open the context menus with none JavaScript. That might appear to be this:
<button popovertarget="context">
Toggle Menu
</button>
<div popover="auto" id="context">
<ul>
<li><button>Save to your Favored Songs</button></li>
<li>
<button popovertarget="playlist">
Add to Playlist
</button>
</li>
<li>
<button popovertarget="share">
Share
</button>
</li>
</ul>
</div>
<div popover="auto" id="share">...</div>
<div popover="auto" id="playlist">...</div>
Now, you may outline a position-fallback
and share it between the context menus. We ensure that to unset any inset
kinds 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 {
@strive {
high: anchor(high);
left: anchor(proper);
}
@strive {
high: anchor(backside);
left: anchor(proper);
}
@strive {
high: anchor(high);
proper: anchor(left);
}
@strive {
backside: anchor(backside);
left: anchor(proper);
}
@strive {
proper: anchor(left);
backside: anchor(backside);
}
}
@position-fallback --flip {
@strive {
backside: anchor(high);
left: anchor(left);
}
@strive {
proper: anchor(proper);
backside: anchor(high);
}
@strive {
high: anchor(backside);
left: anchor(left);
}
@strive {
high: anchor(backside);
proper: anchor(proper);
}
}
This offers you an adaptive nested context menu UI. Attempt altering the content material place with the choose. The choice you select updates grid alignment. And that impacts how anchor positioning positions the popovers.
# Focus and comply with
This demo combines CSS primitives by bringing in :has(). The concept is to transition a visual indicator for the enter
that has focus.
Do that by setting a brand new anchor at runtime. For this demo, a scoped customized property will get up to date on enter focus.
#e-mail {
anchor-name: --email;
}
#identify {
anchor-name: --name;
}
#password {
anchor-name: --password;
}
:root:has(#e-mail:focus) {
--active-anchor: --email;
}
:root:has(#identify:focus) {
--active-anchor: --name;
}
:root:has(#password:focus) {
--active-anchor: --password;
}:root {
--active-anchor: --name;
--active-left: anchor(var(--active-anchor) proper);
--active-top: calc(
anchor(var(--active-anchor) high) +
(
(
anchor(var(--active-anchor) backside) -
anchor(var(--active-anchor) high)
) * 0.5
)
);
}
.form-indicator {
left: var(--active-left);
high: var(--active-top);
transition: all 0.2s;
}
However, how would possibly you are taking this additional? You would use it for some type of tutorial overlay. A tooltip may transfer between factors of curiosity and replace its content material. You would crossfade the content material. Discrete animations permitting you to animate display
or View Transitions may work right here.
# Bar chart calc
One other enjoyable factor you are able to do with anchor positioning is mix it with calc
. Think about a chart the place you will have some popovers that annotate the chart.
You would observe the best and lowest values utilizing CSS min
and max
. The CSS for that might look one thing like this:
.chart__tooltip--max {
left: anchor(--chart proper);
backside: max(
anchor(--anchor-1 high),
anchor(--anchor-2 high),
anchor(--anchor-3 high)
);
translate: 0 50%;
}
There’s some JavaScript at play to replace the chart values and a few CSS to model the chart. However anchor positioning takes care of the format updates for us.
# Resize Handles
You do not have to solely anchor to at least one ingredient. You would use many anchors for a component. You might need observed that within the bar chart instance. The tooltips have been anchored to the chart after which the suitable bar. In the event you took that idea just a little additional you might use it to resize components.
You would deal with the anchor factors like customized resize handles and lean into an inset
worth.
.container {
place: absolute;
inset:
anchor(--handle-1 high)
anchor(--handle-2 proper)
anchor(--handle-2 backside)
anchor(--handle-1 left);
}
On this demo, GreenSock Draggable makes the handles Draggable. However, the <img>
ingredient resizes to fill the container that adjusts to fill the hole between the handles.
This final one’s a little bit of a tease for what’s to return. However, you may create a focusable popover and now you will have anchor positioning. You would create the foundations of a styleable <choose>
ingredient.
<div class="select-menu">
<button popovertarget="listbox">
Choose possibility
<svg>...</svg>
</button>
<div popover="auto" id="listbox">
<possibility>A</possibility>
<possibility>Styled</possibility>
<possibility>Choose</possibility>
</div>
</div>
An implicit anchor
will make this simpler. However, the CSS for a rudimentary place to begin may appear to be this:
[popovertarget] {
anchor-name: --select-button;
}
[popover] {
anchor-default: --select-button;
high: anchor(backside);
width: anchor-size(width);
left: anchor(left);
}
Mix the options of the Popover API with CSS Anchor positioning and also you’re shut.
It’s neat if you begin introducing issues like :has()
. You would rotate the marker on open:
.select-menu:has(:open) svg {
rotate: 180deg;
}
The place may you are taking it subsequent? What else do we have to make {that a} functioning choose
? We’ll save that for the following article. However don’t fear, styleable choose components are coming. Keep tuned!
# That’s it!
The online platform is evolving. CSS anchor positioning is an important half to enhancing the way you develop UI controls. It should summary you away from a few of these difficult choices. However it’s going to additionally can help you do stuff you’ve by no means been in a position to do earlier than. Similar to styling a <choose>
ingredient! Tell us what you suppose.
Picture by CHUTTERSNAP on Unsplash