Skip to content

Animaciones con scroll ​

Las animaciones ligadas al scroll cuentan una historia mientras el usuario baja por la página: elementos que aparecen, barras que indican el progreso, capas que se mueven a distinta velocidad. Son el sello de las landing pages modernas.

Concepto: tres formas de reaccionar al scroll ​

TécnicaCuándo usarlaRendimiento
IntersectionObserver (JS)Disparar algo una vez cuando un elemento entra en pantallaExcelente: no se ejecuta en cada píxel de scroll
Evento scroll + requestAnimationFrame (JS)Efectos continuos (parallax) en navegadores sin soporte modernoBueno si sólo cambias transform
Scroll-driven animations (CSS: animation-timeline)Animaciones atadas al progreso del scroll, sin JSExcelente: corre fuera del hilo principal

No hagas cálculos pesados en scroll

El evento scroll puede dispararse decenas de veces por segundo. Si dentro lees medidas (getBoundingClientRect) y cambias estilos de layout, la página se traba. Usa { passive: true }, agrupa los cambios con requestAnimationFrame y modifica sólo transform/opacity.

Los demos se desplazan dentro del recuadro

Haz scroll dentro de la vista previa para ver los efectos, o ábrelos en una pestaña nueva con el enlace inferior.

Aparecer al hacer scroll (IntersectionObserver) ​

La técnica más usada (y lo que hace la librería AOS por dentro), en ~15 líneas de JS.

Reveal con IntersectionObserver
js
const observer = new IntersectionObserver((entradas) => {
  entradas.forEach((entrada) => {
    if (entrada.isIntersecting) {
      entrada.target.classList.add('visible');
      observer.unobserve(entrada.target);   // animar sólo la primera vez
    }
  });
}, { threshold: 0.15, rootMargin: '0px 0px -40px 0px' });

document.querySelectorAll('.reveal').forEach((el) => observer.observe(el));
css
.js .reveal { opacity: 0; transform: translateY(50px); transition: opacity .8s, transform .8s; }
.js .reveal.visible { opacity: 1; transform: none; }

Mejora progresiva con la clase .js

El contenido sólo se oculta si existe la clase .js en <html>, que añade el propio script. Si JavaScript falla o está desactivado, todo se ve normalmente. Nunca dejes contenido invisible que dependa de JS para aparecer.

Barra de progreso de lectura ​

Indica cuánto del artículo has leído. Ideal para blogs y documentación.

Barra de progreso de lectura

En navegadores modernos es CSS puro:

css
.barra-lectura {
  position: fixed; top: 0; left: 0; right: 0; height: 5px;
  transform-origin: left;
  animation: crecer linear both;
  animation-timeline: scroll(root);   /* el progreso = la posición del scroll */
}
@keyframes crecer { from { transform: scaleX(0); } to { transform: scaleX(1); } }

Para los que no lo soportan, el script calcula scrollY / (scrollHeight - innerHeight) y lo guarda en una variable CSS. Se comprueba con CSS.supports('animation-timeline: scroll()') y con @supports en el CSS.

Parallax por capas ​

Las capas lejanas se mueven más lento que las cercanas, creando profundidad. Cada capa tiene un data-vel entre 0 y 1.

Parallax con capas
js
addEventListener('scroll', () => {
  if (!pendiente) { pendiente = true; requestAnimationFrame(actualizar); }
}, { passive: true });

function actualizar() {
  capas.forEach((c) => c.style.transform = `translate3d(0, ${scrollY * c.dataset.vel}px, 0)`);
  pendiente = false;
}

El patrón pendiente + requestAnimationFrame garantiza como máximo un cálculo por fotograma, aunque el evento scroll se dispare más veces. Con prefers-reduced-motion el parallax se desactiva por completo (es de los efectos que más mareo provocan).

Sticky reveal: tarjetas apiladas ​

Con position: sticky, cada tarjeta se queda pegada arriba y la siguiente se apila encima. Si el navegador soporta scroll-driven animations, las de atrás además se encogen y oscurecen.

Tarjetas apiladas con sticky
css
.tarjeta {
  position: sticky;
  top: calc(20px + var(--i) * 14px);   /* cada tarjeta asoma un poco */
}
@supports (animation-timeline: view()) {
  .tarjeta {
    animation: alejar linear both;
    animation-timeline: view();
    animation-range: exit-crossing 0% exit-crossing 100%;
  }
}

La parte base (sticky) funciona en todos los navegadores sin JavaScript; el encogido es una mejora progresiva.

Scroll-driven animations con fallback ​

animation-timeline: view() ata una animación a la visibilidad del propio elemento: si subes, la animación retrocede. Es como AOS, pero en CSS y reversible.

animation-timeline: view() + fallback
css
@supports (animation-timeline: view()) {
  .item {
    animation: aparecer linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 40%;  /* de cuando empieza a entrar hasta el 40 % */
  }
}

El demo indica si tu navegador lo soporta. Si no, añade la clase .fallback y usa IntersectionObserver con transiciones normales.

Soporte de navegadores

Chrome, Edge y Opera soportan scroll-driven animations desde 2023, y Safari las incorporó después. Firefox las tiene tras una bandera en algunas versiones. Consulta caniuse.com y usa siempre @supports con un fallback.

Ejercicios ​

  1. Añade al reveal una variante .desenfoque que pase de filter: blur(10px) a blur(0).
  2. Haz que la barra de lectura cambie de color al llegar al final (pista: añade más pasos al @keyframes).
  3. Agrega una cuarta capa de nubes al parallax con data-vel="0.7".
Ver solución del ejercicio 1
css
.js .reveal.desenfoque { filter: blur(10px); transition: opacity .8s, filter .8s; }
.js .reveal.desenfoque.visible { filter: blur(0); }

Resumen ​

  • IntersectionObserver para animar al entrar en pantalla: eficiente y sencillo.
  • Si escuchas scroll: passive: true + requestAnimationFrame + sólo transform.
  • animation-timeline: scroll() / view() = animaciones de scroll en CSS puro; envuélvelas en @supports.
  • Oculta contenido sólo si JS está activo (clase .js) y desactiva el parallax con reduced-motion.

Academia Web — aprende, copia y construye.