I was checking out a very cool art-directed article the other day, full of scrollytelling, and, like us web devs will be forever cursed to do, wondering what they used to build it. Spoiler: it’s GSAP and ScrollTrigger.
No shame in those tech choices, they are great. But with scroll-driven animations now being a web standard with growing support, it begs the question whether we could do this with native technologies.
My brain focused on one particular need of the scrollytelling style:
- While the page scrolls through a particular section
- Have a child element appear in a fixed position and be animated
- … but before and after this section is being scrolled through, the element is hidden
Perhaps a diagram can help drive that home:
But I was immediately confused when thinking about how to do this with scroll-driven animations. The problem is that that “section” itself is the thing we need to apply the animation-timeline: view();
to, such that we have the proper moment to react to (“the section is currently in view!“). But in my diagram above, it’s actually a <blockquote>
that we need to apply special conditional styling to, not the section. In a @keyframe
animation, all we can do is change declarations, we can’t select other elements. Apologies if that confusing, but the root of is that we need to transfer styles from the section to the blockquote without using selectors — and it’s weird.
The good news is that what we can do is update CSS custom properties on the section, and those values will cascade to all the children of the section, and we can use those to style the blockquote.
First, in order to make a custom property animatable, we need to declare it’s type. Let’s do a fade in first, thus we need opacity:
@property --blockquoteOpacity {
syntax: "<percentage>";
inherits: true;
initial-value: 0%;
}
Code language: CSS (css)
Now the section itself has the animation timeline:
section.has-pullquote {
animation: reveal linear both;
animation-timeline: view();
animation-range: cover 0% cover 100%;
}
Code language: CSS (css)
And that animation we’ve named reveal
above can now update the custom property:
@keyframes reveal {
from {
--blockquoteOpacity: 0%;
}
to% {
--blockquoteOpacity: 100%;
}
}
Code language: CSS (css)
Now as the animation runs, based on it’s visibility in the viewport, it will update the custom property and thus fade/in out the blockquote:
blockquote {
opacity: var(--blockquoteOpacity);
position: sticky;
top: 50%;
transform: translateY(-50%);
}
Code language: CSS (css)
Note I’m using position: sticky
in there too, which will keep our blockquote in the middle of the viewport while we’re cruising through that section.
Try it out (Chrome ‘n’ friends have stable browser support):
Here’s a video of it working in case you’re in a non-supporting browser:
Because we instantiated the opacity custom property for the opacity at 100%, even in a non-supporting browser like Safari, the blockquote will be visible and it’s a fine experience.
I found this all a little fiddly, but I’m not even sure I’m doing this “correctly”. Maybe there is a way to tap into another elements view timeline I’m not aware of? If I’m doing it the intended way, I could see this getting pretty cumbersome with lots of elements and lots of different values needing updated. But after all, that’s the job sometimes. This is intricate stuff and we’re using the CSS primitives directly. The control we have is quite fine-grained, and that’s a good thing!
Very much looking forward to the days when this is supported in Firefox and Safari! We use a lightweight scroll-driven script for simple position-based animation and sometimes reach for Locomotive for more complicated interactions, but a CSS solution would be a very nice improvement.
In both Chrome proper and a Chromium browser (Vivaldi), it doesn’t quite work as the video shows. (Version 130.0.6723.70 on Win11, I don’t know if that makes any sort of difference…)
The blockquote first shows up at full opacity when the paragraph appears at the bottom of the viewport, then fades out, then reappears and goes red as intended.
Works fine if initial-value for –blockquoteOpacity is set to 0% instead, so may need to set that separately for non-supporting browsers instead?
Ooops yeah I screwed something up there while tweaking it. should be good now. You’re right in that initial value needs to be 0% for the opacity and to set it to 100% in a non-support scenario like:
“Maybe there is a way to tap into another elements view timeline I’m not aware of?”
Yes, a named ViewTimeline on the section would be better here. You then use that name as the animation-timeline for the blockquote.
This is covered in episode 5 of the free to watch “Unleash the Power of Scroll-Driven Animations” video course: https://scroll-driven-animations.style/#learn
Yesssss that’s great. I’ve had a play with it and it works as expected, mostly. I’ll update things here soon. I was hoping because you name the timelines with a custom ident that you could use it ANYWHERE in the DOM, but it looks like no, where you use the named timeline still has to be a descendent. Plus sometimes you still need to animate custom properties just for the ergonomics of it, like animating that text gradient is still animating a custom property territory.
To make
view-timeline-name
more broadly visible there istimeline-scope
you can use.You use this on a shared parent to hoist the visibility of the name.
This is also covered in episode 5 of the video series: https://www.youtube.com/watch?v=Dk1YA8dCgE0&list=PLNYkxOF6rcICM3ttukz9x5LCNOHfWBVnn&index=5&t=444s
The fill effect can also be done on a char by char basis https://codepen.io/thebabydino/pen/zYmVrRL
Not trying to knock your approach, but I’m confused about why the scrollytelling section is placed in the middle of other divs. This setup seems prone to breaking if the container div or inner elements of the block/pullquote section change in width.
I’m still not convinced about avoiding vanilla JS or GSAP for the effect rather than wrestling with CSS to make it fully native. I usually prefer CSS sticky with adjusted div heights and mix in GSAP for animations when needed, though I’ve had issues with GSAP and ScrollTrigger in the past and try not to over-rely on them.
I’m not quite sure you mean about the widths, but I think your point is fair. If you’re more comfortable with an animation library, then you’re more comfortable with an animation library. When the platform gives us new tools, I think it’s worth exploring to figure out what they do well, where the rough edges are, and make choices based on that experimentation.