The State of CSS 2024 survey wrapped up and the results are interesting, as always. Even though each section is worth analyzing, we are usually most hyped about the section on the most used CSS features. And if you are interested in writing about web development (maybe start writing with us 😉), you will specifically want to check out the feature’s Reading List section. It holds the features that survey respondents wish to read about after completing the survey and is usually composed of up-and-coming features with low community awareness.
One of the features I was excited to see was my 2024 top pick: CSS Anchor Positioning, ranking in the survey’s Top 4. Just below, you can find Scroll-Driven Animations, another amazing feature that gained broad browser support this year. Both are elegant and offer good DX, but combining them opens up new possibilities that clearly fall into what most of us would have considered JavaScript territory just last year.
I want to show one of those possibilities while learning more about both features. Specifically, we will make the following blog post in which footnotes pop up as comments on the sides of each text.
For this demo, our requirements will be:
- Pop the footnotes up when they get into the screen.
- Attach them to their corresponding texts.
- The footnotes are on the sides of the screen, so we need a mobile fallback.
The Foundation
To start, we will use the following everyday example of a blog post layout: title, cover image, and body of text:
The only thing to notice about the markup is that now and then we have a paragraph with a footnote at the end:
<main class="post">
<!-- etc. -->
<p class="note">
Super intereseting information!
<span class="footnote"> A footnote about it </span>
</p>
</main>
In that demo, the footnotes are located inside the body of the post just after the text we want to note. However, we want them to be attached as floating bubbles on the side of the text. In the past, we would probably need a mix of absolute and relative positioning along with finding the correct inset properties for each footnote.
However, we can now use anchor positioning for the job, a feature that allows us to position absolute elements relative to other elements — rather than just relative to the containment context it is in. We will be talking about “anchors” and “targets” for a while, so a little terminology as we get going:
- Anchor: This is the element used as a reference for positioning other elements, hence the anchor name.
- Target: This is an absolutely-positioned element placed relative to one or more anchors. The target is the name we will use from now on, but you will often find it as just an “absolutely positioned element” in other resources.
I won’t get into each detail, but if you want to learn more about it I highly recommend our Anchor Positioning Guide for complete information and examples.
The Anchor and Target
It’s easy to know that each .footnote
is a target element. Picking our anchor, however, requires more nuance. While it may look like each .note element should be an anchor element, it’s better to choose the whole .post
as the anchor. Let me explain if we set the .footnote
position to absolute:
.footnote {
position: absolute;
}
You will notice that the .footnote
elements on the post are removed from the normal document flow and they hover visually above their .note
elements. This is great news! Since they are already aligned on the vertical axis, we just have to move them on the horizontal axis onto the sides using the post as an anchor.
This is when we would need to find the correct inset property to place them on the sides. While this is doable, it’s a painful choice since:
- You would have to rely on a magic number.
- It depends on the viewport.
- It depends on the footnote’s content since it changes its width.
Elements aren’t anchors by default, so to register the post as an anchor, we have to use the anchor-name
property and give it a dashed-ident (a custom name starting with two dashes) as a name.
.post {
anchor-name: --post;
}
In this case, our target element would be the .footnote
. To use a target element, we can keep the absolute positioning and select an anchor element using the position-anchor
property, which takes the anchor’s dashed ident. This will make .post
the default anchor for the target in the following step.
.footnote {
position: absolute;
position-anchor: --post;
}
Moving the Target Around
Instead of choosing an arbitrary inset value for the .footnote
‘s left
or right
properties, we can use the anchor()
function. It returns a <length>
value with the position of one side of the anchor, allowing us to always set the target’s inset properties correctly. So, we can connect the left side of the target to the right side of the anchor and vice versa:
.footnote {
position: absolute;
position-anchor: --post;
/* To place them on the right */
left: anchor(right);
/* or to place them on the left*/
right: anchor(left);
/* Just one of them at a time! */
}
However, you will notice that it’s stuck to the side of the post with no space in between. Luckily, the margin
property works just as you are hoping it does with target elements and gives a little space between the footnote target and the post anchor. We can also add a little more styles to make things prettier:
.footnote {
/* ... */
background-color: #fff;
border-radius: 20px;
margin: 0px 20px;
padding: 20px;
}
Lastly, all our .footnote
elements are on the same side of the post, if we want to arrange them one on each side, we can use the nth-of-type()
selector to select the even
and odd
notes and set them on opposite sides.
.note:nth-of-type(odd) .footnote {
left: anchor(right);
}
.note:nth-of-type(even) .footnote {
right: anchor(left);
}
We use nth-of-type()
instead of nth-child
since we just want to iterate over .note
elements and not all the siblings.
Just remember to remove the last inset declaration from .footnote
, and tada! We have our footnotes on each side. You will notice I also added a little triangle on each footnote, but that’s beyond the scope of this post:
The View-Driven Animation
Let’s get into making the pop-up animation. I find the easiest part since both view and scroll-driven animation are built to be as intuitive as possible. We will start by registering an animation using an everyday @keyframes
. What we want is for our footnotes to start being invisible and slowly become bigger and visible:
@keyframes pop-up {
from {
opacity: 0;
transform: scale(0.5);
}
to {
opacity: 1;
}
}
That’s our animation, now we just have to add it to each .footnote
:
.footnote {
/* ... */
animation: pop-up linear;
}
This by itself won’t do anything. We usually would have set an animation-duration
for it to start. However, view-driven animations don’t run through a set time, rather the animation progression will depend on where the element is on the screen. To do so, we set the animation-timeline
to view()
.
.footnote {
/* ... */
animation: pop-up linear;
animation-timeline: view();
}
This makes the animation finish just as the element is leaving the screen. What we want is for it to finish somewhere more readable. The last touch is setting the animation-range
to cover 0% cover 40%
. This translates to, “I want the element to start its animation when it’s 0% in the view and end when it’s at 40% in the view.”
.footnote {
/* ... */
animation: pop-up linear;
animation-timeline: view();
animation-range: cover 0% cover 40%;
}
This amazing tool by Bramus focused on scroll and view-driven animation better shows how the animation-range
property works.
What About Mobile?
You may have noticed that this approach to footnotes doesn’t work on smaller screens since there is no space at the sides of the post. The fix is easy. What we want is for the footnotes to display as normal notes on small screens and as comments on larger screens, we can do that by making our comments only available when the screen is bigger than a certain threshold, which is about 1200px
. If it isn’t, then the notes are displayed on the body of the post as any other note you may find on the web.
.footnote {
display: flex;
gap: 10px;
border-radius: 20px;
padding: 20px;
background-color: #fce6c2;
&::before {
content: "Note:";
font-weight: 600;
}
}
@media (width > 1200px) {
/* Styles */
}
Now our comments should be displayed on the sides only when there is enough space for them:
Wrapping Up
If you also like writing about something you are passionate about, you will often find yourself going into random tangents or wanting to add a comment in each paragraph for extra context. At least, that’s my case, so having a way to dynamically show comments is a great addition. Especially when we achieved using only CSS — in a way that we couldn’t just a year ago!