Skip to main content

June 24, 2024

Enhancing Webflow with GSAP: A Guide to Fade-In and Stagger Animations

4 min read

Introduction

When it comes to creating stunning websites, animations play a crucial role in enhancing user experience. While Webflow offers built-in animation capabilities, integrating GSAP (Green Sock Animation Platform) can elevate your site to new heights. In this post, we'll explore how to create Fade-in and Stagger animations using GSAP and why this approach offers advantages over native Webflow animations.

Why Choose GSAP Over Webflow Animations?

(1) Performance and Flexibility

GSAP is known for its high performance and efficiency, even with complex animations. This ensures that your animations run smoothly without compromising the website's load times. GSAP's flexibility allows for a wide range of animation effects that are often more sophisticated than what Webflow's native tools can provide.

(2) Ease of Use and Detailed Control

With GSAP, developers have precise control over every aspect of an animation. This includes timing, easing, and sequencing. Such detailed control can be challenging to achieve with Webflow's more simplified interface. GSAP also supports timeline-based animations, which can be incredibly powerful for creating intricate sequences.

(3) Cross-Browser Compatibility

GSAP ensures that your animations look and perform consistently across all major browsers. This is particularly beneficial for ensuring a seamless experience for all users, regardless of their browser choice.

Getting Started with GSAP Animations in Webflow

Setting Up GSAP in Webflow

To start using GSAP in your Webflow project, you'll first need to include the GSAP library and the ScrollTrigger plugin. This can be done by adding the relevant CDN links to the head section of your Webflow project.

1. Add to a custom code embed

I add all my code into a code embed element that I turn into a component and place at the top of every page I intend to utilize this animation on.

__wf_reserved_inherit

2. Include GSAP Library:

<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>

3. Register ScrollTrigger Plugin:

You want to make sure that you register your plugin at the top of your code.

Also, notice I wrap everything inside of the window.Webflow... command. This ensures that Webflow loads before the code runs.

You also want to make sure that you wrap everything inside of a script tag so Webflow will load the script.

<script>
window.Webflow ||= [];
window.Webflow.push(() => {
  gsap.registerPlugin(ScrollTrigger);
  
});
</script>

Creating a Fade-In Animation

Here is the code you will utilize for the Fade-In Animation.

Make sure you add the custom attribute to the element you want to fade-in.

///////////////////// FADE IN ANIMATION ////////////////

  const fadeInElements = gsap.utils.toArray("[fade-in]");

  fadeInElements.forEach((fadeInElement) => {
    gsap.fromTo(
      fadeInElement,
      { opacity: 0, y: 20 }, // Initial state
      {
        // Final state
        opacity: 1,
        y: 0,
        duration: 0.5,
        scrollTrigger: {
          trigger: fadeInElement,
          start: "top 60%", // When top of element reaches 60% in the viewport
          end: "bottom 50%", // When bottom of element reaches 50% in the viewport
          //markers: "true", // Turn markers on (uncomment) to see where the element will start the animation. Be sure to turn off when publishing to the production domain or they will show on the live website. 
        },
      }
    );
  });

  ///////////////////// END FADE IN ANIMATION ////////////////

Creating a Stagger Animation

Here is the code you will utilize for the Fade-In Animation.

Make sure you add the custom attribute to the list that houses the children elements for the children you want to fade-in on stagger.

///////////////////// BEGIN FADE LIST ANIMATION ////////////////

  const fadeInLists = gsap.utils.toArray("[fade-list]");

  fadeInLists.forEach((fadeInList) => {
    gsap.fromTo(
      fadeInList.children,
      { opacity: 0, y: 20 }, // Initial state

      {
        // Final state
        opacity: 1,
        y: 0,
        duration: 0.5,
        stagger: 0.2,
        scrollTrigger: {
          trigger: fadeInList,
          start: "top 60%", // When top of element reaches 60% in the viewport
          end: "bottom 50%", // When bottom of element reaches 50% in the viewport
          // markers: "true", // Turn markers on (uncomment) to see where the element will start the animation. Be sure to turn off when publishing to the production domain or they will show on the live website. 
        },
      }
    );
  });

  ///////////////////// END FADE LIST ANIMATION ////////////////

Refresh Scroll Trigger

In order to make sure that Scroll Trigger runs correctly, you can refresh it after the page loads. I have included two options for this refresh.

The first option uses setTimeout and the second options uses the load event.

The load event has the advantage of not waiting for an arbitrary time to refresh the page, but rather waits until everything has been loaded before refreshing Scroll Trigger.

As well as you can refresh it manually. Be cautious about refreshing Scroll Trigger manually as it can hinder performance.

Also, you will not need this step if you have add width and height to each image, as well as set an aspect ratio on each image. If you have done that, you won't need the manual refresh, but I'm including it just in case you run into issues.

Use the Potato extension to help add width and height to your images and for reference when setting up custom aspect ratios.

Here is the code for both the setTimeout refresh and the manual refresh.

//////////// REFRESH SCROLLTRIGGER after everything has loaded //////////////
  
  // Pick between these two. The load event is a great option to ensure everything is loaded. 
  
  /////////// OPTION 1 - setTimeout ///////////
  //Using a setTimeout to ensure everything has loaded on the page.

  setTimeout(() => {
    ScrollTrigger.refresh(true);
    console.log("Scroll Trigger has been refreshed");
  }, 1500); //Refreshes ScrollTrigger after 1.5 seconds to ensure everything has completed loaded and is settled into place.
  
   ///////// OPTION 2 - Load event ///////// 
   
   //Using the 'load' event to ensure everything has loaded on the page

  window.addEventListener('load', () => {
    ScrollTrigger.refresh(true);
    console.log("Scroll Trigger has been refreshed");
  });
  ///////////// MANUAL SCROLLTRIGGER REFRESH /////////////////

  //Meant to esnure Fade In Animiation above runs properly. Placed selectively to refresh scrollTrigger on sections that were affecting scrollTrigger animations afterwards. These are typically sections that have fade-in animations where images are loading, or sections that have dynamic content that needs to load. If proper aspect-ratio's are applied to all images on the site, you should not need this option.

  //But if you need to use it, place refresh in the attributes panel on the element at which you want Scroll Trigger to be refreshed. No value is needed.

  const refreshElements = gsap.utils.toArray("[refresh]");

  refreshElements.forEach((refreshElement) => {
    const id = refreshElement.id; // Get the ID of the element
    const message = `${id} has refreshed` || "section refreshed - no ID added";
    const refresh = gsap.timeline({
      scrollTrigger: {
        trigger: refreshElement,
        start: "top bottom",
        onLeave: () => {
          console.log(message);
          ScrollTrigger.refresh();
        },
      },
    });
  });

All the code in one block

Paste this code into your custom code embed, make it a component and place it on every page you want the animations to occur.

<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js"></script>

<script>
window.Webflow ||= [];
window.Webflow.push(() => {
  gsap.registerPlugin(ScrollTrigger);

  ///////////////////// FADE IN ANIMATION ////////////////

  const fadeInElements = gsap.utils.toArray("[fade-in]");

  fadeInElements.forEach((fadeInElement) => {
    gsap.fromTo(
      fadeInElement,
      { opacity: 0, y: 20 }, // Initial state
      {
        // Final state
        opacity: 1,
        y: 0,
        duration: 0.5,
        scrollTrigger: {
          trigger: fadeInElement,
          start: "top 60%", // When top of element reaches 60% in the viewport
          end: "bottom 50%", // When bottom of element reaches 50% in the viewport
          //markers: "true", // Turn markers on (uncomment) to see where the element will start the animation. Be sure to turn off when publishing to the production domain or they will show on the live website. 
        },
      }
    );
  });

  ///////////////////// END FADE IN ANIMATION ////////////////

  ///////////////////// BEGIN FADE LIST ANIMATION ////////////////

  const fadeInLists = gsap.utils.toArray("[fade-list]");

  fadeInLists.forEach((fadeInList) => {
    gsap.fromTo(
      fadeInList.children,
      { opacity: 0, y: 20 }, // Initial state

      {
        // Final state
        opacity: 1,
        y: 0,
        duration: 0.5,
        stagger: 0.2,
        scrollTrigger: {
          trigger: fadeInList,
          start: "top 60%", // When top of element reaches 60% in the viewport
          end: "bottom 50%", // When bottom of element reaches 50% in the viewport
          // markers: "true", // Turn markers on (uncomment) to see where the element will start the animation. Be sure to turn off when publishing to the production domain or they will show on the live website. 
        },
      }
    );
  });

  ///////////////////// END FADE LIST ANIMATION ////////////////

  //////////// REFRESH SCROLLTRIGGER after everything has loaded //////////////
  
  // Pick between these two. The load event is a great option to ensure everything is loaded. 
  
  /////////// OPTION 1 - setTimeout ///////////
  //Using a setTimeout to ensure everything has loaded on the page.

  setTimeout(() => {
    ScrollTrigger.refresh(true);
    console.log("Scroll Trigger has been refreshed");
  }, 1500); //Refreshes ScrollTrigger after 1.5 seconds to ensure everything has completed loaded and is settled into place.
  
   ///////// OPTION 2 - Load event ///////// 
   
   //Using the 'load' event to ensure everything has loaded on the page

  window.addEventListener('load', () => {
    ScrollTrigger.refresh(true);
    console.log("Scroll Trigger has been refreshed");
  });

  ///////////// MANUAL SCROLLTRIGGER REFRESH /////////////////

  //Meant to esnure Fade In Animiation above runs properly. Placed selectively to refresh scrollTrigger on sections that were affecting scrollTrigger animations afterwards. These are typically sections that have fade-in animations where images are loading, or sections that have dynamic content that needs to load. If proper aspect-ratio's are applied to all images on the site, you should not need this option.

  //But if you need to use it, place refresh in the attributes panel on the element at which you want Scroll Trigger to be refreshed. No value is needed.

  const refreshElements = gsap.utils.toArray("[refresh]");

  refreshElements.forEach((refreshElement) => {
    const id = refreshElement.id; // Get the ID of the element
    const message = `${id} has refreshed` || "section refreshed - no ID added";
    const refresh = gsap.timeline({
      scrollTrigger: {
        trigger: refreshElement,
        start: "top bottom",
        onLeave: () => {
          console.log(message);
          ScrollTrigger.refresh();
        },
      },
    });
  });
});
</script>

Conclusion

Integrating GSAP with Webflow unlocks a world of possibilities for creating sophisticated and high-performance animations.

The level of control, flexibility, and cross-browser consistency provided by GSAP makes it a powerful tool for any web developer looking to enhance their Webflow projects.

By leveraging GSAP for animations, you can deliver a more engaging and dynamic user experience that stands out from the competition.

As well as you can experience a more performant website. Not having to load Webflow's animation library can really increase the overall load time of your website. Give it a try and see which wins in a speed test. Just be sure to completely remove all Webflow interactions from the website before testing the GSAP only alternative.

marketing health check clipboard showing score

Your Marketing Isn't Bringing in Leads and You Can't Figure Out Why

Score your marketing across 7 key areas in 5 minutes and find out exactly what's holding back your leads and what to fix first.

Questions You Might Be Asking

They technically coexist, but they both target the same elements' transform and opacity, so you'll get fighting animations and unpredictable results. Pick one per element. If you're switching to GSAP for performance reasons, remove all Webflow IX2 interactions from the elements you're animating, and ideally from the site altogether so the IX2 runtime isn't loading.

It delays your code until Webflow finishes its own setup — CMS rendering, element hydration, and IX2 initialization. Without it, GSAP can fire before Webflow has laid out the DOM, and ScrollTrigger will measure the wrong positions. Wrapping inside Webflow.push() is the safest way to avoid those timing bugs.

Whenever layout shifts after the initial measurements — late-loading fonts, CMS content, images without dimensions, or accordions opening can all throw off the positions ScrollTrigger cached. The fix is ScrollTrigger.refresh(). If you set width, height, and aspect-ratio on every image, most of these issues disappear without any manual refresh.

It can — if you fully remove IX2 from the site. Webflow's interactions runtime is a decent chunk of JavaScript that runs regardless of whether you use it everywhere. Swapping to GSAP + ScrollTrigger for a lean, purpose-built animation set often ships less code overall. Test both versions in Lighthouse with IX2 fully removed to see the real difference.

Stop losing deals to a website that's holding you back.

Book a 30-minute strategy session. You'll leave with a clearer picture of what your site needs to do and a rough budget range, whether you hire us or not.

background cta image collage of websites

Other posts in this category

video thumbnail showcasing video and how to build trust with testimonials in the background

How to build trust quickly with potential clients

Building trust with potential clients is something that will be the difference between making a sale or not. Here's how you can build trust quickly.

A three-way comparison graphic: three website mockups side by side labeled DIY Builder, Template, and Custom, each with a small price tag and a "best when" note beneath it, and an arrow underneath showing the progression as a business grows.

Is a Custom Website Worth It, or Should I Use a Template or DIY Builder?

Custom, template, or DIY builder? Here's how the three options compare, plus a simple decision framework.

confetti over Vimeo Video playing

How to Add a Confetti Animation When a Vimeo Video Starts Playing

Learn how to trigger a canvas confetti animation when a Vimeo video starts playing using the Vimeo Player API play event