Scroll Trigger with Minimal JavaScript

A real mouse sitting on a computer mouse with a colorful mouse mat

The web / browser capabilities have come a long way, things previously only possible with JavaScript can now be done with regular CSS and with a little bit of creativity we can push it even further. When doing Scroll Triggers one obvious way is to use Intersection Observers and set a class name on the node to fire off the animation.

Let's do it differently and use a new shiny thing called animation-timeline. It's a CSS property that allows us to do scroll driven animations without JavaScript.

It's a new feature so it has limited support.

But it can still be used as progressive enhancement, we can see an example of it below, it uses the scroll position as the scrubber for the animation. So when the element is within the viewport the animation is connected to the scroll position.

Rocket Rocket flame
Rocket outburst flames
CSS
  
  
    
<div class="scroll-driven__example">
<div class="scroll-driven__rocket">
<img class="scroll-driven__rocket-body" src="/images/rocket.webp" alt="Rocket" />
<img class="scroll-driven__rocket-flame" src="images/rocket-flame.webp" alt="Rocket flame" />
</div>
<img class="scroll-driven__rocket-base" src="/images/rocket-base.webp" alt="Rocket outburst flames" />
</div>

The images are animated using regular css @keyframes. A container is used to set the stage for the animation, but also for controlling the progress. If each image had it's own animation-timline: view(); they would be harder to coordinate because each element will enter the viewport at different times. By using a single view-timeline-name we can coordinate against the same element to synchronize the animations.

Rocket launch code is available on codepen.

CSS
  
  
    
/* CSS omitted for brevity, focusing on relevant parts.
Check codepen link for a complete working example. */

@keyframes rocket {
0% {
transform: translateY(0);
}

100% {
transform: translateY(-700px);
}
}

@keyframes flame {
0% {
scale: 1 1;
opacity: 1;
}

100% {
scale: 0.5 0.8;
opacity: 0.5;
}
}

@keyframes base {
0% {
transform: scaleX(1) scaleY(1);
opacity: 1;
}

75% {
opacity: 1;
}

100% {
transform: translate3d(0, 100px, 0) scaleX(3) scaleY(0.5);
opacity: 0;
}
}

.scroll-trigger__example {
view-timeline-name: --rocket;
view-timeline-axis: block;

/* optimize rendering by isolating */
contain: paint layout;
}

.scroll-trigger__rocket-base {
animation: base 10s linear forwards;
animation-timeline: --rocket;
animation-range: entry 70% cover 75%;
}

.scroll-trigger__rocket-flame {
transform-origin: top center;
animation: flame 10s cubic-bezier(.69, -0.15, .5, .46) forwards;
animation-range: entry 70% cover 75%;
animation-timeline: --rocket;
}

.scroll-trigger__rocket {
animation: rocket 10s cubic-bezier(.69, -0.15, .5, .46) forwards;
animation-timeline: --rocket;
animation-range: entry 70% cover 75%;
}

Now let's (mis)use to trigger animation on scroll

We have to setup some event listeners and rely on a naming convention. When the animation starts we add animate class on the container element. When combined with animation timeline: view(); the animation will start when things comes into view so it scroll triggers.

JS
 
  
    
function setupEventListeners() {
document.addEventListener('animationstart', ({ animationName, target }) => {
if (animationName.startsWith('scroll-trigger')) {
target.classList.add('animate');
}
});
}

document.addEventListener('DOMContentLoaded', setupEventListeners);

We use our naming convention and create an empty keyframe animation: @keyframes scroll-trigger--fire-off {}. Then make it trigger when it comes into view. Using the animationstart event we can add a class to the container element.

CSS
  
  
/* CSS omitted for brevity, focusing on relevant parts.
Check codepen link for a complete working example. */

@keyframes scroll-trigger--fire-off {}

.scroll-trigger__example {
display: grid;
place-items: center;
position: relative;
overflow: clip;
height: var(--rocket-container-height);

border: 1px solid white;
border-radius: 100px;

background-color: #ddd;
box-shadow: inset 2px 7px 10px 0 rgba(0, 0, 0, 0.5);
animation: scroll-trigger--fire-off;
animation-timeline: view();
animation-range: entry 100%;
}

.animate .scroll-trigger__rocket-base {
animation: base 10s linear forwards;
}

.animate .scroll-trigger__rocket-flame {
animation: flame 10s cubic-bezier(.69, -0.15, .5, .46) forwards;
}

.animate .scroll-trigger__rocket {
animation: rocket 10s cubic-bezier(.69, -0.15, .5, .46) forwards;
}

.animate .scroll-trigger__rocket-base {
animation: base 10s linear forwards;
}

.animate .scroll-trigger__rocket-flame {
animation: flame 10s cubic-bezier(.69, -0.15, .5, .46) forwards;
}

.animate .scroll-trigger__rocket {
animation: rocket 10s cubic-bezier(.69, -0.15, .5, .46) forwards;
}
Scroll past this box and the animation will start.
Rocket Rocket flame
Rocket outburst flames

It's pretty easy to setup and now we can configure any scroll triggers directly from our CSS. That's pretty neat, right?

Complete scroll trigger code available on codepen.

Quick Recap

When supported, animation-timeline: view(); can be used to create scroll driven animations when an element comes into view, with regular CSS.

Because the animation won't start until it enters the view, events like animationstart will also be deferred. We can create empty @keyframes with namning conventions like scroll-trigger-x and listen for animations that has this pattern then add class names to fire off other animations.

In the future we might do this directly from CSS.