wandres.dev
GSAP XI · Interacción

ScrollSmoother y el debate sobre el scroll suavizado

Cómo funciona, qué markup exige, los efectos por atributo, y una exposición honesta de los argumentos a favor y en contra del scroll suavizado.

⏱ 20 min

Pocas técnicas dividen tanto a la profesión. Para una parte del oficio, el scroll suavizado es la diferencia entre una web y una experiencia; para otra, es la manifestación más pura de que el diseño se ha puesto por delante del usuario. Ambos bandos tienen argumentos reales y ninguno está siendo deshonesto. Esta lección explica cómo funciona la herramienta, en qué se diferencia de las implementaciones que dieron mala fama a la técnica, y expone los dos casos con la mayor limpieza posible para que decidas tú.

🎯 Al terminar esta lección sabrás
  • Montar ScrollSmoother con el markup que requiere y entender qué transforma.
  • Usar los efectos por atributo para paralaje y retardo sin escribir animaciones.
  • Enumerar los argumentos a favor y en contra del scroll suavizado.
  • Identificar qué problemas de accesibilidad resuelve esta implementación y cuáles no.

Cómo funciona y qué exige

ScrollSmoother está construido sobre ScrollTrigger, así que hay que registrar los dos. Y exige una estructura de markup concreta: un envoltorio exterior y un contenido interior que envuelva todo.

<body>
  <div id="smooth-wrapper">
    <div id="smooth-content">
      <!-- TODO el contenido de la pagina va aqui -->
    </div>
  </div>
  <!-- Los elementos position: fixed van FUERA -->
</body>
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { ScrollSmoother } from "gsap/ScrollSmoother";

gsap.registerPlugin(ScrollTrigger, ScrollSmoother);

const smoother = ScrollSmoother.create({
  wrapper: "#smooth-wrapper",
  content: "#smooth-content",
  smooth: 1.2,
  effects: true,
  smoothTouch: 0.1,
});

El mecanismo es este: la página sigue teniendo scroll nativo real, con su barra y su altura, y lo que hace ScrollSmoother es aplicar una transformación matrix3d al contenido para que su posición visible persiga con retraso a la posición nativa del scroll. Si tú estás en el píxel 1000 según el navegador, el contenido puede estar mostrando el 940 y acercándose.

Esa es la diferencia técnica clave con las bibliotecas de scroll suavizado que dieron mala fama a la técnica hace años. Aquellas cancelaban el scroll del navegador y lo reimplementaban a mano, con lo que perdían la barra, las teclas de navegación, la búsqueda en página y el anclaje por fragmento. Aquí el scroll nativo sigue existiendo y funcionando; solo la presentación va con retraso.

Los valores por defecto que conviene conocer: smooth vale 0,8 segundos; smoothTouch está desactivado por defecto, es decir, en dispositivos táctiles no hay suavizado ninguno salvo que lo pidas; y ease vale "expo".

ScrollSmoother.get() recupera la instancia, y solo puede existir una a la vez. Debe crearse antes que tus ScrollTriggers.

⚠️
position: fixed dentro del contenido no funciona

El elemento de contenido lleva una transformación aplicada, y eso crea un bloque contenedor nuevo. Cualquier hijo con position: fixed se fija a ese contenedor y se mueve con el scroll. Es la misma trampa de la fijación de ScrollTrigger, y la solución es la misma: saca esos elementos fuera del envoltorio, o usa pin de ScrollTrigger en lugar de position: fixed.

Los efectos por atributo

Con effects: true, ScrollSmoother busca elementos con los atributos data-speed y data-lag y les aplica el efecto correspondiente sin que escribas ninguna animación.

data-speed multiplica la velocidad a la que el elemento se mueve respecto al scroll: 0.5 va a la mitad, 2 al doble, 1 es normal. data-speed="auto" calcula el valor a partir de cuánto puede moverse el elemento dentro de su contenedor, que es la forma correcta de hacer paralaje de imágenes de fondo con el contenedor en overflow: hidden.

data-lag hace que el elemento tarde ese número de segundos en alcanzar su posición, produciendo un efecto de arrastre.

<img src="fondo.jpg" data-speed="0.6" alt="">
<h2 data-speed="1.2">Titular que adelanta</h2>
<div class="tarjeta" data-lag="0.3">Se queda un poco atrás</div>

<div class="marco" style="overflow: hidden">
  <img src="paisaje.jpg" data-speed="auto" alt="">
</div>

Dos detalles operativos. Los elementos alcanzan su posición natural cuando están centrados verticalmente en el viewport, no cuando entran; por eso un elemento con data-speed distinto de 1 aparece desplazado respecto a su posición en el flujo si está arriba o abajo del todo. Desde la versión 3.12 se puede envolver el valor en clamp()data-speed="clamp(0.5)"— para que los elementos que ya están en la primera pantalla arranquen en su posición natural.

Y una advertencia que la documentación repite dos veces: los efectos no se deben anidar. Un elemento con data-speed dentro de otro con data-speed produce resultados incorrectos.

También hay equivalente por código, que es lo que quieres si los elementos se crean dinámicamente:

smoother.effects(".tarjeta", { speed: 0.8, lag: 0.2 });

El debate, en dos columnas

El caso a favor

Los argumentos de quienes lo defienden son estos, y son reales.

Suaviza la entrada escalonada de la rueda de ratón. Una rueda de ratón no produce scroll continuo: produce saltos de decenas de píxeles. Sin suavizado, cualquier animación atada al scroll avanza a escalones. Es el mismo argumento que justifica el scrub numérico, aplicado a la página entera.

Sincroniza el hilo de scroll con el hilo principal. Los navegadores gestionan el repintado del scroll en otro hilo, y por eso un elemento fijado por ScrollTrigger puede temblar o saltar en el momento de fijarse. Con ScrollSmoother, la posición visible del contenido la calcula tu código en el mismo tick que las animaciones, y esos artefactos desaparecen. Este argumento es puramente técnico y no tiene nada que ver con el gusto.

Da acceso a los efectos por atributo. El paralaje y el retardo con un atributo en el HTML son una reducción real de código respecto a montar un ScrollTrigger por elemento.

Resuelve problemas de móvil. normalizeScroll: true fuerza el scroll al hilo de JavaScript, lo que evita que la barra de direcciones aparezca y desaparezca en la mayoría de dispositivos y elimina el rebote de sobredesplazamiento.

Preserva el scroll nativo. A diferencia de implementaciones anteriores, la barra existe, las teclas funcionan, y la posición se restaura al navegar. La documentación oficial lo plantea como su ventaja principal.

El caso en contra

Los argumentos de quienes lo rechazan también son reales, y merecen el mismo espacio.

Introduce latencia deliberada entre la mano y la pantalla. Es el argumento central y no tiene refutación técnica: con smooth: 1, la página tarda un segundo en llegar donde el usuario le pidió estar. Para quien busca información en lugar de contemplar una experiencia, eso es fricción pura. Y a diferencia de una animación, que ocurre una vez, esta latencia está en todas las interacciones de scroll de toda la sesión.

Rompe la calibración motora del usuario. La gente tiene interiorizada la relación entre cuánto gira la rueda y cuánto se mueve la página, y esa calibración se aplica sin pensar. Cambiarla obliga a recalibrar en cada sitio, y el resultado se percibe como que la página “no responde bien” aunque no se sepa señalar por qué.

Puede provocar malestar vestibular. El movimiento continuo desacoplado de la entrada del usuario es uno de los patrones que la preferencia de movimiento reducido pretende evitar. Y aquí no basta con desactivar una animación: hay que desactivar el mecanismo entero.

Depende de JavaScript para algo que el navegador ya hacía. Si el script falla, tarda o es bloqueado, la página se queda con una estructura de envoltorios cuyo comportamiento sin la biblioteca puede ser incorrecto.

Interfiere con la navegación por teclado y con la búsqueda en página. El scroll nativo se conserva, sí, pero la posición visible va con retraso. Al buscar una palabra con el buscador del navegador, el resultado se resalta y la página tarda en llegar. Al tabular a un elemento, ocurre lo mismo. La documentación ofrece onFocusIn para intervenir, y devolver false desde ahí impide que ScrollSmoother desplace el elemento a la vista, pero es una intervención que hay que escribir.

Cambia la estructura del documento. Todo el contenido pasa a estar dentro de dos envoltorios con transformación, lo que rompe position: fixed, complica position: sticky y obliga a revisar cualquier CSS que dependa del body como contenedor.

Una postura operativa

No hay una respuesta correcta, pero sí hay una forma de tomar la decisión que evita las dos versiones perezosas —ponerlo porque queda bien y rechazarlo por principio—.

Pregúntate qué hace la gente en tu página. Si viene a leer, a comparar precios, a rellenar un formulario o a buscar un dato, la latencia le cuesta y el efecto no le aporta. Si viene a ver un porfolio, una presentación de producto o una pieza editorial concebida como recorrido, el argumento cambia.

Y si lo pones, hazlo condicional y con valores moderados.

const mm = gsap.matchMedia();

mm.add("(prefers-reduced-motion: no-preference)", () => {
  const smoother = ScrollSmoother.create({
    smooth: 0.6,          // moderado, no 2
    effects: true,
    smoothTouch: false,   // en tactil, nada
    normalizeScroll: false,
  });
  return () => smoother.kill();
});

Con prefers-reduced-motion en reduce, ScrollSmoother ni se crea, y la página tiene scroll nativo puro. smoothTouch: false respeta el scroll táctil del sistema, que ya tiene su propia física y con el que competir nunca sale bien. Y un smooth de 0,6 da la mayoría del beneficio técnico con una fracción de la latencia.

El scroll es el único control de una página que el usuario ya sabía usar antes de llegar, y modificarlo tiene un coste que no aparece en ninguna métrica

Hay una forma de plantear este debate que lo hace más fácil de resolver, y consiste en fijarse en qué tipo de control estás modificando. Casi todo lo que hay en una interfaz es un control que el usuario tiene que aprender: dónde está el menú, qué hace ese icono, cómo se filtra esta lista. El scroll no. El scroll es un control que el usuario trae aprendido de las otras diez mil páginas que ha visitado, con una calibración motora afinada durante años y ejecutada sin atención consciente. Es, junto con el clic, uno de los dos únicos gestos verdaderamente universales de la web. Modificar un control aprendido tiene un coste distinto al de introducir uno nuevo, y es un coste que ninguna métrica captura: nadie rellena una encuesta diciendo “el scroll iba raro”, simplemente la sesión es un poco peor y la atención se gasta un poco antes. La contrapartida es que el scroll también es el eje sobre el que se construye casi toda la narrativa visual de la web moderna, y hay piezas —un porfolio, la presentación de un producto físico, un reportaje— en las que el recorrido es el contenido, y donde una décima de segundo de latencia compra una continuidad que de otro modo no existe. Eso hace que la pregunta correcta no sea “¿es bueno el scroll suavizado?”, que no tiene respuesta, sino “¿es esta página un documento o es un recorrido?”. En un documento, el usuario tiene un objetivo y el scroll es el camino: cualquier fricción en el camino es coste puro. En un recorrido, el scroll es la propia experiencia y suavizarlo es tan legítimo como elegir la duración de un plano. Lo que no es defendible es no haberse hecho la pregunta, poner un smooth: 2 porque venía así en la demo, y descubrir tres meses después por analítica que la gente abandona antes de llegar al segundo bloque.

⚔️ Decidir con datos propios
  1. Monta ScrollSmoother sobre una página existente y navega con la rueda, con el trackpad y con las teclas de página. Anota qué se siente distinto en cada caso.
  2. Prueba smooth a 0,3, 0,8 y 2 y busca el punto en que la latencia empieza a molestarte.
  3. Busca una palabra con el buscador del navegador en una página con smooth: 1.5 y describe la experiencia.
  4. Recorre la página solo con la tecla de tabulación y comprueba qué hace el foco.
  5. Envuélvelo en matchMedia con la preferencia de movimiento reducido y verifica que con la preferencia activada la página tiene scroll nativo puro.