← All articles

[ Blog ]

June 24, 2026

5 min read

Smooth Scroll in Next.js with GSAP & Lenis

A practical guide to buttery 60fps smooth scroll in Next.js using Lenis and GSAP ScrollTrigger — setup, scroll-linked animation, and the performance gotchas.

Next.jsGSAPLenisAnimationPerformance
Smooth Scroll in Next.js with GSAP & Lenis

Smooth scrolling is one of those details that separates a generic template from a site that feels crafted. Done well, it makes scroll-linked animation feel weighty and intentional. Done badly, it fights the browser, drops frames, and breaks accessibility. This guide shows how to add smooth scroll to Next.js the way award-winning sites do it — with Lenis for the scroll itself and GSAP ScrollTrigger for animation — including the gotchas nobody mentions.

We'll target the App Router (Next.js 14+), keep it 60fps, and respect prefers-reduced-motion.

Why Lenis instead of CSS or a scroll library

You have three broad options for smooth scroll, and they are not equal.

ApproachFeelPerformanceControl
scroll-behavior: smooth (CSS)Only smooths anchor jumps, not the wheelGreatNone
Heavy "smooth scrollbar" librariesSmooth but often janky, hijacks layoutMixedMedium
LenisNatural inertial wheel/touch smoothingExcellentFull

Lenis won the creative-dev community because it normalises the scroll (wheel, trackpad, touch) into a single eased value without faking the scrollbar or breaking native scrolling. It also exposes a per-frame value you can pipe straight into GSAP — which is exactly what you want for scroll-linked animation.

1. Install

npm install lenis gsap

2. A Lenis provider for the App Router

Smooth scroll is a client concern, so wrap your app in a small client component. The key trick for performance: drive Lenis and GSAP from a single requestAnimationFrame loop so they share one time base and never fight.

// app/lenis-provider.tsx
'use client';

import { ReactLenis, type LenisRef } from 'lenis/react';
import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

export function LenisProvider({ children }: { children: React.ReactNode }) {
  const lenisRef = useRef<LenisRef>(null);

  useEffect(() => {
    function raf(time: number) {
      lenisRef.current?.lenis?.raf(time);
      ScrollTrigger.update();
    }
    gsap.ticker.add(raf);
    gsap.ticker.lagSmoothing(0);
    return () => gsap.ticker.remove(raf);
  }, []);

  return (
    <ReactLenis root options={{ autoRaf: false }} ref={lenisRef}>
      {children}
    </ReactLenis>
  );
}

Then mount it in your root layout, inside <body>:

// app/layout.tsx
import { LenisProvider } from './lenis-provider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <LenisProvider>{children}</LenisProvider>
      </body>
    </html>
  );
}

That's it — you now have inertial smooth scroll across the whole site.

3. Scroll-linked animation with ScrollTrigger

Because Lenis updates ScrollTrigger every frame, your animations are perfectly in sync with the eased scroll. A pinned, scrubbed section looks like this:

'use client';
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import { gsap } from 'gsap';

export function Reveal() {
  const ref = useRef<HTMLDivElement>(null);

  useGSAP(() => {
    gsap.to('.panel', {
      yPercent: -100,
      ease: 'none',
      scrollTrigger: {
        trigger: ref.current,
        start: 'top top',
        end: '+=150%',
        scrub: true,
        pin: true,
      },
    });
  }, { scope: ref });

  return <div ref={ref}>{/* .panel children */}</div>;
}

scrub: true ties the animation's progress directly to scroll position, so the eased Lenis value carries straight into the motion. This is the same technique behind the pinned showcase on most case-study sites.

4. The performance gotchas

This is where most implementations fall apart. Keep these in mind:

  • Animate only transform and opacity. They run on the compositor and won't trigger layout. Animating top, width, or margin on scroll will tank your frame rate.
  • One RAF loop. If Lenis runs its own requestAnimationFrame and GSAP runs its ticker, you get two loops and subtle jitter. The provider above disables Lenis's autoRaf and drives both from GSAP's ticker.
  • Call ScrollTrigger.refresh() after layout changes (font load, route change, images settling) so trigger positions stay correct.
  • Lazy-load below-the-fold work. Smooth scroll makes long pages tempting; keep Core Web Vitals healthy by deferring heavy components.

5. Accessibility: respect reduced motion

Smooth scroll and big scroll animations can cause motion sickness. Always honour the user's preference — disable the easing and the animations when prefers-reduced-motion: reduce is set:

const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// e.g. pass `options={{ smoothWheel: !reduced }}` to Lenis,
// and guard ScrollTrigger animations behind `if (!reduced) { ... }`.

GSAP's gsap.matchMedia() makes this clean — register reduced-motion variants and GSAP swaps them automatically.

FAQ

Does Lenis hurt SEO or Core Web Vitals?

No. Lenis smooths the visual scroll without blocking the main thread when set up with a single RAF loop. Content is still server-rendered HTML, so crawling and indexing are unaffected. Keep your animations on transform/opacity and CLS/INP stay healthy.

Does it work with the Next.js App Router and SSR?

Yes. The provider is a client component, so it hydrates on the client while your pages stay server-rendered. Mount it once in the root layout.

How do I anchor-scroll to a section?

Use lenis.scrollTo('#section') (or a numeric offset). Lenis intercepts the jump and eases to the target, so in-page navigation matches the rest of the experience.


Smooth scroll is a small amount of code with an outsized impact on perceived quality. Pair Lenis with GSAP, keep everything on the compositor, drive it from one loop, and respect reduced motion — and you get the kind of weighty, intentional feel that makes people stay.

Want this built into your product? See how I work with teams.