Scrollytelling with GSAP ScrollTrigger

Scrollytelling with GSAP ScrollTrigger

Scrollytelling with GSAP ScrollTrigger

Scrollytelling with GSAP ScrollTrigger

Scrollytelling with GSAP ScrollTrigger

Scrollytelling with GSAP ScrollTrigger

Show Scroll Markers

GSAP is a JavaScript library that makes it easy to code high-performance and complex animations. GSAP has a flexible interface that is easy to use with D3 and other common dataviz libraries. GSAP has just released a new plugin called ScrollTrigger that facilitates scroll-driven animations. ScrollTrigger can be used along with GSAP’s own animation functions, but you can also use it just as a scroll watcher to trigger any function (for example, run some D3 code) on a particular scroll interaction. This document showcases how you can use ScrollTrigger to power some common dataviz scrollytelling patterns, but the library is extremely full-featured and flexible, you can see more about what’s possible in the docs. Oh, and if you want to see all the scroller and trigger markers for each scroll interaction in this demo, use the menu at the top right ↗.

See the source code for this demo!

*Disclaimer: ScrollTrigger is very new, and hence I have had very little practice with it and I doubt it has even been used by anyone in a real dataviz project yet. That means there’s very few best practices at this point. What follows works for me, but it is subject to change, and I make no claims that this is the best or “correct” way to do things.


One common scrollytelling pattern is to have a chart become ‘pinned’, while boxes of explainer text are scrolled, like this. GSAP has a built-in method for pinning elements like this. This chart is pinned by creating a new ScrollTrigger which triggers when the #chart-wrapper element reaches the middle of the viewport. The trigger sets pin: true to pin the trigger element (in this case our chart wrapper) in place until the end of the trigger, which is set to the bottom of the last text box.
            
ScrollTrigger.create({
    trigger: '#chart-wrapper',
    endTrigger: '#step-4',
    start: 'center center',
    end: () => {
        const height = window.innerHeight;
        const chartHeight = document.querySelector('#chart-wrapper').offsetHeight;
        return `bottom ${chartHeight + (height - chartHeight) / 2}px`;
    },
    pin: true,
    pinSpacing: false
});
            
        
You can then trigger GSAP animations. Here, we’re using a normal gsap.to() tween to animate the cx attribute of our points. To trigger the animation on scroll, we just add a scrollTrigger object to our normal GSAP tween. The animation fires when the second text box (our trigger ) crosses the middle of the screen, and reverses when you scroll backwards. Using GSAP tweens has the advantage that you can easily pause, restart, complete, or reverse your tweens using toggleActions.
            
gsap.to('.points', {
    scrollTrigger: {
        trigger: '#step-2',
        start: 'top center',
        toggleActions: 'play none none reverse'
    },
    cx: () => Math.random() * svgWidth,
    duration: 0.5,
    ease: 'power3.inOut'
});
            
        
You can also just use GSAP to set the ScrollTrigger, and let D3 or another library handle the animation. In this case, we set up a new ScrollTrigger with GSAP, and passed it callback functions (circlesToTimeline and circlesToRandom) which contain our custom D3 animation code via the arguments onEnter (forward animation) and onLeaveBack onLeaveBack (backwards animation). The onEnter callback will fire when our trigger crosses the scroller start from the top, and onLeaveBack will fire when the trigger crosses the scroller start from the bottom.
            
ScrollTrigger.create({
    trigger: '#step-3',
    start: 'top center',
    onEnter: circlesToTimeline,
    onLeaveBack: circlesToRandom
});
            
        
Also, notice how all these text boxes are transitioning from transparent to opaque when they enter and leave the viewport? That’s GSAP too. We set up a ScrollTrigger for each box (it was easy with GSAP’s toArray function and a forEach loop), and used the toggleClass argument to toggle an ‘active’ CSS class (which just adds opacity to the element) whenever the element is in view.
            
gsap.utils.toArray('.step').forEach(step => {
    ScrollTrigger.create({
        trigger: step,
        start: 'top 80%',
        end: 'center top',
        toggleClass: 'active'
    });
});
            
        

So far we’ve been animating SVG elements, but GSAP can animate components of basically any renderer: SVG, DOM, Canvas, or WebGL. And it’s built to work with popular libraries like pixi.js and three.js. So, let’s try using it to animate some DOM elements, in this case a bunch of stacked text blocks. We’ll also show off another cool features of ScrollTrigger—scrubbing. In the examples above, the whole animation fired as soon as the trigger was activated, but you can also link the animation progress to the scroll progress, so that when you’re scrolled 50% into an element, the animation is 50% complete. In GSAP, this is as easy as setting scrub: true on your scrollTrigger. You can also set scrub to a number, which will basically put a delay on the animation so that it catches up to the scroll position that many seconds later. Here we’ve stacked six text blocks, each colored differently, and set them to move down the page with a scrubbing animation. Each text block has a slight scrub delay (the purple text has almost no delay, and the red text has the longest delay), to demonstrate the ‘catch-up’ feature. The code looks like this:

            
gsap.utils.toArray('.scrub').forEach((el, i) => {
    gsap.to(el, {
        scrollTrigger: {
            trigger: '.scrub-wrapper',
            start: 'top top',
            end: 'bottom center+=150',
            pin: '.scrub-wrapper',
            scrub: (7 - i) * 0.1
        },
        y: '45vh'
    });
});
            
        

scroll me!

scroll me!

scroll me!

scroll me!

scroll me!

scroll me!

This has barely scratched the surface of what’s possible with GSAP and ScrollTrigger, but hopefully you can see how powerful this library is for dataviz and scrollytelling. Although this post has emphasized how simple ScrollTrigger makes all of this, the truth is that there’s always gotcha’s that come up with scrollytelling, it’s usually not that simple. Making sure that your CSS and HTML structure don’t fight with the scroll interactions can take some practice and trial and error. I’ve also found that GSAP animations work quite differently than D3 animations, so it can be tricky to mix these two. ScrollTrigger also has quite a bewildering array of options for configuration. This makes it far more flexible and powerful than most other scrollytelling libraries, but it also means that setting up animations can take some getting used to.

This demo is just to show off what’s possible and give some code examples, but notice I didn’t talk much about the details, like setting triggers or start and end points. You can read more about the details, other options, and helpful tips for ScrollTrigger on my blog. You can also see the full source code for this demo on my GitHub.