GSAP In Practice: Avoid The Pitfalls

Jean-Baptiste Kaiser
#css#js

In the process of enhancing our landing page with dynamic elements using the GSAP animation library, we encountered several challenges. This article discusses these issues and provides the solutions we discovered.

This blog post is the last of the series we started to share our experience with the GSAP animation library:

  1. GSAP Basics: Dive into Web Animations
  2. Trigger Animations On Scroll With GSAP
  3. GSAP in practice: some pitfalls to avoid (this post)

We recommend you check out the two previous posts if you haven't already, to better understand the concepts discussed in the following sections.

Addressing SVG Compatibility Issues

If you are getting serious with animations, it probably won't be long before you find yourself trying to animate some SVGs. And why wouldn't you? SVGs are a great way to get creative with graphics, and offer immense abilities to animate sub-parts of the canvas, curves, colors, or in fact any of their attributes.

But you may run into an issue if you are planning to use CSS transforms, just like I did: various browsers are not consistent in the way they deal with transforms, and especially, don't honor the transformOrigin attribute properly.

See the Pen Working with SVG by @slax57 on CodePen.

Fortunately, there exist techniques to harmonize those behaviors across browsers. And the good news is, they're already baked into GSAP!

So, instead of setting the following CSS directly on your elements, which might not be consistent across all browsers:

.rect {
  transform-origin: 50% 50%;
}

We just need to do this:

gsap.set(".rect", { transformOrigin: '50% 50%' });

And we are ready to animate our canvas.

Restarting A ScrollTrigger Animation

What happens to scroll-based animations when users scroll back up? We discovered that it can be tricky to reset an animation properly. For instance, try scrolling down and then back up in the following CodePen:

See the Pen Restarting A ScrollTrigger Animation by @slax57 on CodePen.

The reset of the animation is visible and creates an unpleasant flash.

As we mentioned in the previous post, we leveraged toggleActions to reset an animation when the user scrolls backward passed the trigger element.

gsap.to(".box", {
  scrollTrigger: { 
    trigger: ".box",
    toggleActions: "play pause resume reset",
  },
  x: 500,
});

The problem arises when we combine this with a start option to start playing the animation only when the top of the trigger element reaches, say, 70% down from the top of the viewport.

gsap.to(".box", {
  scrollTrigger: { 
    trigger: ".box",
    toggleActions: "play pause resume reset",
    start: "top 70%",
  },
  x: 500,
});

As far as I can tell, there is no simple combination of toggleActions, start, and end that will allow to have both the animation starting at 70% of the viewport and resetting when it's out of it.

The only solutions I found are:

  1. Instead of resetting the animation with reset, use reverse to play it in reverse and then avoid the flash.
  2. Use two different scroll triggers, one to start the animation with start: "top 70%", and the second to reset it (with the default start value).
  3. Do not reset the animation at all (i.e. play it only once).

Below is an example demonstrating how we can use two different scroll triggers to achieve that effect:

const myTween = gsap.to(".box", {
  x: 400,
});

ScrollTrigger.create({
  trigger: ".box",
  animation: myTween,
  start: "top 70%",
  toggleActions: "play pause resume none",
});

ScrollTrigger.create({
  trigger: ".box",
  animation: myTween,
  //start is left to its default value
  toggleActions: "none none none reset",
});

See the Pen Restarting A ScrollTrigger Animation - The Fix by @slax57 on CodePen.

Avoiding FOUC When Running Animations On Load

It can be nice to trigger some animations on page load, making the main elements of the page appear with a catchy and appealing animation, especially in the hero section of a page.

To implement this animation, you may be tempted to reveal the hero section with a from tween on the alpha channel, like this:

<div id="hero">My hero section</div>
<script>
gsap.from(
    '#hero',
    { autoAlpha: 0, y: '15%' },
);
</script>

Unfortunately, there is an issue with this implementation: it will create a Flash Of Unstyled Content (FOUC). Indeed, the #hero element will first be rendered with an opacity of 1, and then immediately after the JS has loaded, GSAP will apply an opacity of 0 to it, to start the animation.

One way around this issue could be to already apply opacity: 0 in the element's styles, and then use fromTo to animate from an opacity of 0 to 1.

<div id="hero" style="opacity: 0;">
  My hero section
</div>
<script>
gsap.fromTo(
    '#hero',
    { autoAlpha: 0, y: '15%' },
    { autoAlpha: 1 },
);
</script>

But this solution creates an additional problem. Bots, or browsers that don't have JavaScript enabled, will never see our hero element, since it will stay at opacity: 0.

One way to solve that last issue is to undo the hiding inside of a <noscript> tag.

<noscript>
  <style>#hero { opacity: 1 !important; }</style>
</noscript>
<div id="hero" style="opacity: 0;">
  My hero section
</div>
<script>
gsap.fromTo(
    '#hero',
    { autoAlpha: 0, y: '15%' },
    { autoAlpha: 1 },
);
</script>

This way, browsers with JavaScript disabled will override the style of #hero and set its opacity to 1, whereas other browsers will ignore that style and run the animation from an opacity of 0 like normal.

Adapting For Mobile

It may seem trivial to some of you, but I innocently enough thought I would be able to reuse the animations that run well in desktop mode in the mobile version of our website.

Unfortunately, that's not always possible. Indeed, since the elements are (most of the time) arranged with a different disposition on mobile, you will sometimes have no choice but to adapt the positioning, direction, timing and even triggers for your animations for mobile mode.

This may result in slightly less synthetic code, and will probably require a lot of testing with the different resolutions to find a solution that optimizes for simplicity and compatibility.

The other issue I came across is that GSAP will not always update the animation objects properly when the viewport dimensions change (e.g. when you resize the browser window). This can cause issues, especially with ScrollTrigger, where the animations do not trigger properly or are offset.

The easiest way I found to solve such issues was to leverage gsap.context() to properly revert all animations before re-applying them after the context has changed.

With React, it can be used in a useEffect hook like this:

export const MyComponent = () => {
    const rootComponent = useRef();
    useEffect(() => {
        let ctx = gsap.context(() => {
            gsap.to(".box", { x: 100 });
        }, rootComponent);

        // cleanup
        return () => ctx.revert();
    }, []);

    return (
        <div ref={rootComponent}>
            <div class="box">A box</div>
        </div>
    );
};

Alternatively, at the time of writing this blog post, I learned about the useGSAP hook, which seems to be a cleaner and simpler way to solve this same issue.

With it, the previous example could be simplified to the following:

export const MyComponent = () => {
    const rootComponent = useRef();
    useGSAP(() => {
      gsap.to(".box", { x: 100 });
    }, { scope: rootComponent });

    return (
        <div ref={rootComponent}>
            <div class="box">A box</div>
        </div>
    );
};

I unfortunately did not have enough time to experiment with it yet, but it looks very promising so I hope to get the chance to revisit my code someday to use it.

Conclusion

As we saw, working with web animations can uncover some unexpected difficulties. Fortunately, for most of them, GSAP offers a solution and as such, fulfills its promise to take the complexity away from you. This allows you to focus your time and attention on finding how to best animate the elements of your web page (which can still be very time-consuming 😅).

All in all, I'm still very pleased with my general experience with GSAP, and I'm convinced it is a very good candidate when choosing an animation library for most applications.

Did you like this article? Share it!