Built for Motion
Log in
All posts

Smooth Scrolling with Lenis and GSAP ScrollTrigger in Next.js

A small provider that runs Lenis on GSAP's ticker, so smooth scrolling and ScrollTrigger share one clock and pinned sections stop shuddering.

Lenis gives a site that weighted, expensive scroll. ScrollTrigger drives animations from the scroll position. Run them on two different clocks and every scrubbed or pinned section lags a frame behind the content. The fix is to let GSAP's ticker drive Lenis.

The provider

components/smooth-scroll.tsx
"use client";

import { useEffect, type ReactNode } from "react";
import Lenis from "lenis";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

export function SmoothScroll({ children }: { children: ReactNode }) {
  useEffect(() => {
    if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;

    const lenis = new Lenis({ duration: 1.1 });
    lenis.on("scroll", ScrollTrigger.update);

    const raf = (time: number) => lenis.raf(time * 1000);
    gsap.ticker.add(raf);
    gsap.ticker.lagSmoothing(0);

    return () => {
      gsap.ticker.remove(raf);
      lenis.destroy();
    };
  }, []);

  return children;
}

Wrap your layout with it:

app/layout.tsx
<body>
  <SmoothScroll>{children}</SmoothScroll>
</body>

Why each line is there

  • lenis.on("scroll", ScrollTrigger.update) tells ScrollTrigger the moment the position changes, instead of waiting for a native scroll event.
  • gsap.ticker.add runs Lenis on GSAP's frame loop, so both read the same frame. time arrives in seconds and Lenis wants milliseconds.
  • lagSmoothing(0) stops GSAP from slowing time down after a dropped frame, which would leave the scroll and the animations out of step.
  • prefers-reduced-motion: people who asked for less motion get the browser's native scroll.

Scrollable boxes inside the page

Lenis takes over the mouse wheel for the whole page, so a code block or menu with its own scrollbar stops scrolling. Add data-lenis-prevent to those elements and the wheel scrolls them again.