Skip to main content

Scroll-Driven
 Sections

Chris CoyierOctober 29, 2024About 3 minCSSArticle(s)blogfrontendmasters.comcss

Scroll-Driven
 Sections ꎀ렚

CSS > Article(s)

Article(s)

Scroll-Driven
 Sections
If you're creating a scroll-driven animation and the goal is

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:

  1. While the page scrolls through a particular section
  2. Have a child element appear in a fixed position and be animated
  3. 
 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%;
}

Now the section itself has the animation timeline:

section.has-pullquote {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: cover 0% cover 100%;
}

And that animation we’ve named reveal above can now update the custom property:

@keyframes reveal {
  from {
    --blockquoteOpacity: 0%;
  }
  to% {
    --blockquoteOpacity: 100%;
  }
}

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%);
}

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!

Scroll-Driven
 Sections

If you're creating a scroll-driven animation and the goal is