Saltar al contenido
← BlogDesarrollo14 de octubre de 2026 · 2 min de lectura

Scroll suave con Lenis y GSAP en Next.js

Cómo acoplar Lenis al ticker de GSAP, mantener ScrollTrigger sincronizado y desactivarlo cuando no corresponde (táctil y movimiento reducido).

El scroll suave bien hecho no se nota: se siente como si la página tuviera inercia. Mal hecho, rompe position: sticky, desincroniza las animaciones y castiga a quien prefiere menos movimiento. Esta es la receta que usamos en proyectos con Next.js, GSAP y Lenis.

La idea: un solo reloj

Lenis calcula la posición suavizada de cada fotograma y GSAP anima con su propio ticker. Si cada uno usa su reloj, aparecen saltos. La solución es que Lenis avance dentro del ticker de GSAP y que ScrollTrigger se actualice cada vez que Lenis se mueve.

"use client";

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

gsap.registerPlugin(ScrollTrigger);

export function SmoothScroll({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    const lenis = new Lenis({
      duration: 1.05,
      easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
    });

    lenis.on("scroll", ScrollTrigger.update);

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

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

  return <>{children}</>;
}

Dos detalles importan. GSAP trabaja en segundos y Lenis en milisegundos, de ahí el time * 1000. Y lagSmoothing(0) evita que GSAP "salte" el tiempo cuando la pestaña se ralentiza, algo que con Lenis se notaría como un tirón.

Cuándo no activarlo

El scroll suave no es para todos los contextos. Nuestra regla:

  • Movimiento reducido: si prefers-reduced-motion: reduce, se usa el scroll nativo.
  • Táctil: en móviles el scroll nativo ya tiene inercia y es más fiable. Activamos Lenis solo con (any-pointer: fine).
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
const fine = matchMedia("(any-pointer: fine)").matches;
if (reduce || !fine) return; // scroll nativo

Ancla, bloqueo y navegación

Tres cosas más que conviene resolver desde el principio:

  1. Anclas: con la opción anchors: true, Lenis intercepta los enlaces #id y los anima.
  2. Bloquear el scroll (menús, modales, preloader): lenis.stop() y lenis.start(), más una clase en html para overflow: hidden.
  3. Cambio de ruta: al navegar, devuelve el scroll a cero con lenis.scrollTo(0, { immediate: true }) y llama a ScrollTrigger.refresh() cuando la nueva página esté montada.

Resumen

Un solo reloj, desactivarlo cuando no ayuda y exponer funciones para bloquear y volver arriba. Con eso el scroll suave deja de ser una fuente de errores y vuelve a ser lo que debe ser: un detalle que casi nadie nota, pero que todos sienten.