Smooth Scrolling and Scroll Interactions in Webflow using Locomotive Scroll and Gsap ScrollTrigger

This article was also published on Waveshape Collective’s Medium

You either hate it or love it. If you’ve ever visited Awwwards, you’ve definitely experienced the typical smooth scrolling effect that seems to have been picking up in popularity in the last few years.

One of the most popular smooth scrolling Javascript libraries is Locomotive Scroll, a fairly simple to use library that seems to be working fairly well.

However, when trying to use any smooth scrolling library together with Webflow’s native scroll interactions, you will stumble upon a lot of problems.

This article tries to battle those problem by providing an introduction to setting up Locomotive Scroll together with Gsap ScrollTrigger in order to be able to get going with custom scroll animations on a smooth scrolled website built in Webflow. Let’s go.

How smooth scrolling libraries work

To my understanding, most smooth scrolling libraries wrap a full web page in a wrapper, hijacks the scroll position of the user and introduces the smooth effect by smoothly animating the wrapper vertically and/or horizontally based on the user’s scroll input.

This hijacking causes issues for Webflow’s interactions.

The problem with smooth scrolling and Webflow animations

Apart from the questionable effect this type of scrolling has on the user experience, using a library such as Locomotive.js in Webflow will cause the native scrolling interactions to stop working correctly.

This is caused by the hijacked scroll position in the browser.

While Webflow reads the user’s “actual” scroll position on the page, the smooth scrolling wrapper may or may not be on a totally different part of the website, thus causing trouble.

Please note that other native interactions such as hovers and clicks aren’t affected by these libraries. It’s only scroll specific interactions and animations that won’t work correctly.

How to solve this problem

One of the many ways of solving this problem is using Gsap.js and its ScrollTrigger plugin.

While Gsap is an animation library that simplifies the process of developing animations in Javascript, the ScrollTrigger plugin contains features that enables the Locomotive’s scroll position to be read correctly and used in custom animations. Super cool.

This however requires you to know the basics of Javascript as opposed to the “no-code” way of Webflow Interactions 2.0. Sorry about that.

Anyway, let’s get started with a working example.

Importing libraries

The first thing we have to do is to import all necessary scripts for Gsap and Locomotive in the head tag of our page.

You can find the current updated CDN links for Gsap by clicking here and for Locomotive by clicking here.

Webflow home settings

As you can see, there’s 4 CDN imports for the following:

  • Gsap
  • The Gsap ScrollTrigger plugin
  • Locomotive
  • Locomotive stylesheet
  • Make sure to import all of these.

Locomotive structure setup

In order for Locomotive scroll to actually scroll, we need to wrap all of our pages in two div containers:

  • A load container
  • A scroll container

Webflow navigator

The load container should have a data-attribute of “data-load-container” while the scroll container should have a data-attribute of “data-scroll-container”.

In Webflow we’re forced to give each data attribute a value. These can be set to anything. In my case it’s simply a “1”.

Webflow navigator

Each section on your page(s) should also have a data attribute of “data-scroll-section”. The same value of “1” can be used for these.

Webflow navigator

There’s even more data attributes that can be used to control for example specific element’s scroll speed etc, however these are totally optional. More details can be found here.

Initialization of libraries

Now let’s initialize both Scrolltrigger and Locomotive. Please note that It’s important that ScrollTrigger is initialized before Locomotive.

gsap.registerPlugin(ScrollTrigger);

var locomotive = new LocomotiveScroll({
  el: document.querySelector("[data-scroll-container]"),
  smooth: true,
  multiplier: 1.0,
  getDirection: true,
});

While ScrollTrigger is very straightforward to initialize, Locomotive has a few options which should be selected.

  1. First we’re referencing a wrapper for the whole page. In this case, it’s our scroll container div with the data attribute of “data-scroll-container”

  2. Then we’re enabling the smooth scrolling with “smooth: true”, followed by the speed of the scrolling called “multiplier” and “getDirections: true”.

In these settings you’re also able to for example enable the smooth scrolling on mobile and tablet breakpoints, both of which are turned off by default. Read more about the different options here.

Please note that it’s important to initialize ScrollTrigger before Locomotive in order for things to work correctly.

Configure ScrollTrigger and Locomotive

The first thing we should do after initialization is to make sure that ScrollTrigger updates each time the user scrolls using Locomotive.

This is done like so:

locomotive.on(“scroll”, () => {
    ScrollTrigger.update();
});

Now we need to give ScrollTrigger a scroller proxy with some configurations in order for it to understand Locomotive’s scrolling positions.

ScrollTrigger.scrollerProxy(“[data-scroll-container]”, {
 scrollTop(value) {
   return arguments.length
     ? locomotive.scrollTo(value, 0, 0)
     : locomotive.scroll.instance.scroll.y;
   },
 getBoundingClientRect() {
   return {
     top: 0,
     left: 0,
     width: window.innerWidth,
     height: window.innerHeight,
   };
 },
 pinType: document.querySelector(“[data-scroll-container]”).style.transform
   ? “transform”
   : “fixed”,
});

Make sure to give the pinType the same scroll container data attribute as you’re using for your scroll container, in this case data-scroll-container.

Now we need to change the default scroller from ScrollTrigger’s default of the viewport to the scrolling container.

ScrollTrigger.defaults({
  scroller: “[data-scroll-container]”,
});

Because of (what I would guess is) some troubles regarding stable scrolling position, scrolling based on breakpoints and or Javascript parsing load times, we want to update Locomotive every time a page refresh or a window resize happens, as well as on page load.

ScrollTrigger.addEventListener(“refresh”, () => locomotive.update());

ScrollTrigger.refresh();

CSS Configuration

Through my time using Locomotive scroll, there’s a couple CSS tips and tricks that should be used in order for you to get the most stable smooth scrolling experience possible.

html.has-scroll-smooth {
  backface-visibility: hidden;
  transform: translateZ(0);
}

[data-scroll-container] {
  perspective: 1px;
}

[data-scroll-container],
[data-scroll-section] {
  transform-style: preserve-3d;
  will-change: transform;
}

For example, the data scroll container should have a perspective of 1px in order for elements to stop disappearing at the top and the bottom of the page while scrolling.

This should work in Chrome, but Firefox might need both the data scroll container and data scroll section attributes set to transform-style: preserve-3d and will-change: transform as shown in the image above.

The automatically assigned class name of has-smooth-scroll on the page’s html tag needs a backface-visibility of hidden and a transform of translateZ(0) for certain bugs to disappear.

Copy and paste the following code to your project settings custom code in the head section.

Gsap Scroll Animation

Now we’re ready to animate. Gsap makes this super easy, but it can be quite a lot to get into if you’re new. Read the docs by clicking here.

In this example, we’ll change the background color of the second section of the page from white to black, as well as its text color from black to white.

I want this to start animating when the section’s top scrolls into view and end when the section is fully visible, similar to how a while scrolling in view Webflow animation would be built.

const tl = gsap
 .timeline({
   scrollTrigger: {
     trigger: “.section.section-second”,
     scrub: true,
     start: “top bottom”,
     end: “bottom bottom”,
   },
 })
 .from(“.section.section-second”, {
   backgroundColor: “#FFFFFF”,
   color: “#000000”,
   ease: “power1”,
 });

This animation is using my second section’s class of “.section.section-second” as its trigger, and starts animating when the section’s top is at the bottom of the viewport. It ends when the section’s bottom is at the bottom of the viewport.

The first CSS property to be animated is backgroundColor, going from white (#FFFFFF) to black, which is its default background color I’ve set in Webflow.

The second property is color, which goes from black (#000000) to white in the same way but reversed colors.

Feel free to check out the working Webflow example by clicking here.

Full code example

Javascript (placed before closing body tag)

<script>
  gsap.registerPlugin(ScrollTrigger);

  var locomotive = new LocomotiveScroll({
   el: document.querySelector("[data-scroll-container]"),
   smooth: true,
   multiplier: 1.0,
   getDirection: true,
  });

  locomotive.on("scroll", () => {
   ScrollTrigger.update();
  });

  ScrollTrigger.scrollerProxy("[data-scroll-container]", {
   scrollTop(value) {
     return arguments.length
     ? locomotive.scrollTo(value, 0, 0)
     : locomotive.scroll.instance.scroll.y;
   },
   getBoundingClientRect() {
     return {
       top: 0,
       left: 0,
       width: window.innerWidth,
       height: window.innerHeight,
     };
   },
   pinType: document.querySelector("[data-scroll-container]").style.transform
     ? "transform"
     : "fixed",
  });

  ScrollTrigger.defaults({
   scroller: "[data-scroll-container]",
  });

  ScrollTrigger.addEventListener("refresh", () => locomotive.update());
  ScrollTrigger.refresh();

  const tl = gsap
   .timeline({
     scrollTrigger: {
     trigger: ".section.section-second",
     scrub: true,
     start: "top bottom",
     end: "bottom bottom",
     },
   })
   .from(".section.section-second", {
     backgroundColor: "#FFFFFF",
     color: "#000000",
     ease: "power1",
   });

</script>

CSS (placed before closing head tag)

<style>

  html.has-scroll-smooth {
   backface-visibility: hidden;
   transform: translateZ(0);
  }

  [data-scroll-container] {
   perspective: 1px;
  }

  [data-scroll-container],
  [data-scroll-section] {
   transform-style: preserve-3d;
   will-change: transform;
  }

</style>

Conclusion

Now we’re done. Hopefully you’ll be able to enjoy working with Locomotive and ScrollTrigger to create your own unique masterpieces of websites from now on.

There is a lot more that you can do using Gsap, so make sure to take a look at it by visiting their site and forum.

Feel free to ask any questions in the comments down below.

Have fun.