Skip to content

Instantly share code, notes, and snippets.

@SuddenDev
Last active October 26, 2022 10:54
Show Gist options
  • Save SuddenDev/7d495e61ca0c25964b6f469802617ad8 to your computer and use it in GitHub Desktop.
Save SuddenDev/7d495e61ca0c25964b6f469802617ad8 to your computer and use it in GitHub Desktop.
React (Gatsby) + Locomotive Scroll + Greensock (Gsap) + Scrolltrigger
// <DIR>/src/hooks/useLocoScroll.js
import gsap from "gsap";
import ScrollTrigger from "gsap/ScrollTrigger";
import { useEffect } from "react";
import LocomotiveScroll from "locomotive-scroll";
// REPLACE THIS WITH YOUR STYLES
import "@styles/locomotive-scroll.scss";
gsap.registerPlugin(ScrollTrigger);
//
// PUT YOUR CONFIG HERE
// --------------------------------
const mobileBreakpoint = 1024;
const scrollOptions = {
// https://github.com/locomotivemtl/locomotive-scroll#instance-options
container: "#___gatsby",
options: {
smooth: true,
smoothMobile: false,
getDirection: true,
touchMultiplier: 2.5,
lerp: 0.15,
class: "is-reveal",
},
};
//-----------------------------
let locoScroll = null;
// Updating LS on scrolltrigger refresh
const lsUpdate = () => {
if (locoScroll) {
locoScroll.update();
}
};
const initScrolltrigger = (scrollEl) => {
if (locoScroll) {
locoScroll.on("scroll", ScrollTrigger.update);
ScrollTrigger.removeEventListener("refresh", lsUpdate);
ScrollTrigger.scrollerProxy(scrollEl, {
scrollTop(value) {
if (locoScroll) {
return arguments.length
? locoScroll.scrollTo(value, 0, 0)
: locoScroll.scroll.instance.scroll.y;
}
return null;
},
scrollLeft(value) {
if (locoScroll) {
return arguments.length
? locoScroll.scrollTo(value, 0, 0)
: locoScroll.scroll.instance.scroll.x;
}
return null;
},
getBoundingClientRect() {
return {
top: 0,
left: 0,
width: window.innerWidth,
height: window.innerHeight,
};
},
// LocomotiveScroll handles things completely differently on mobile devices - it doesn't even transform the container at all! So to get the correct behavior and avoid jitters, we should pin things with position: fixed on mobile. We sense it by checking to see if there's a scroll class on the html-tag.
pinType: document
.querySelector("html")
.classList.contains("has-scroll-smooth")
? "transform"
: "fixed",
});
ScrollTrigger.addEventListener("refresh", lsUpdate);
ScrollTrigger.refresh();
}
};
// LOCO SCROLL HOOK
export default function useLocoScroll() {
useEffect(() => {
const scrollEl = document.querySelector(scrollOptions.container);
locoScroll = new LocomotiveScroll({
el: scrollEl,
...scrollOptions.options,
});
// RESPONSIVE DESTROY / INIT
window.scroll = locoScroll;
window.addEventListener("resize", () => {
// resetting scroll on mobile
if (window.innerWidth < mobileBreakpoint) {
if (locoScroll) {
locoScroll.stop();
locoScroll.destroy();
window.scroll = null;
}
locoScroll = false;
// fetching elements with data-scroll attributes
let els = [
...document.querySelectorAll("[data-scroll-section]"),
...document.querySelectorAll("[data-scroll]"),
];
// resetting inline transform stlyes
els.forEach((el) => {
el.style.transform = "";
});
} else {
// start a new instance and wire up scrolltrigger to it fresh
if (!locoScroll) {
console.log("starting new instance");
locoScroll = new LocomotiveScroll({
el: scrollEl,
...scrollOptions.options,
});
locoScroll.update();
initScrolltrigger(scrollEl);
}
}
});
initScrolltrigger(scrollEl);
return () => {
if (locoScroll) {
ScrollTrigger.removeEventListener("refresh", lsUpdate);
window.scroll = null;
locoScroll.destroy();
locoScroll = null;
console.log("Kill", locoScroll);
}
};
});
}

React (Gatsby) + Locomotive Scroll + Greensock (Gsap) + Scrolltrigger

! use with caution !

! This is not necessarily working out of the box. It's still a bit buggy for me as well. I will update it though, if I or someone finds a bug !

This gist does the following:

  • Setup Locomotive Scroll as a hook for React / Gatsby
  • Configure locoscroll and Scrolltrigger to work together on React
  • On resize kill locoscroll and restart scrolltrigger. Also restart Locomotive scroll when the screen size is larger than the breakpoint

Please let me know, if you find any bugs or improvements

// <DIR>/src/layouts/Layout.jsx
//
// It's optional where you use the hook.
// I do recommend putting it into your layout or most outer component, so it get's used everywhere by default
import React, { useEffect, useRef } from "react";
import { gsap } from "gsap";
import ScrollTrigger from "gsap/ScrollTrigger";
// ...
// ... more import scripts
import useLocoScroll from "../hooks/useLocoScroll";
const GlobalLayout = (props) => {
// init locomotive scroll
// the rest is just for test purposes
useLocoScroll();
const ref = useRef(null);
// This is just an example
useEffect(() => {
setTimeout(() => {
ScrollTrigger.matchMedia({
// desktop
"(min-width: 1025px)": () => {
gsap.to(ref.current, {
color: "red",
scrollTrigger: {
trigger: ref,
markers: true,
scrub: true,
scroller: "#___gatsby", // don't forget to change this to your loco scroll container
start: "top top",
end: "60%",
onUpdate: (self) => console.log("progress:", self.progress),
},
});
},
// mobile
"(max-width: 1024px)": () => {
gsap.to(ref.current, {
color: "blue",
scrollTrigger: {
trigger: ref.current,
markers: true,
pin: true,
scrub: true,
start: "top top",
end: "60%",
onUpdate: (self) =>
console.log("progress mobile:", self.progress),
},
});
},
});
ScrollTrigger.refresh();
});
return (
<div>
<div className="headline" ref="ref">Locoscroll Test</div>
<main className="content">{props.children}</main>
// ..... more stuff here
</div>
);
};
export default GlobalLayout;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment