Tema
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écnica | Cuándo usarla | Rendimiento |
|---|---|---|
IntersectionObserver (JS) | Disparar algo una vez cuando un elemento entra en pantalla | Excelente: no se ejecuta en cada píxel de scroll |
Evento scroll + requestAnimationFrame (JS) | Efectos continuos (parallax) en navegadores sin soporte moderno | Bueno si sólo cambias transform |
Scroll-driven animations (CSS: animation-timeline) | Animaciones atadas al progreso del scroll, sin JS | Excelente: 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.
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.
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.
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.
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.
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
- Añade al reveal una variante
.desenfoqueque pase defilter: blur(10px)ablur(0). - Haz que la barra de lectura cambie de color al llegar al final (pista: añade más pasos al
@keyframes). - 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
IntersectionObserverpara animar al entrar en pantalla: eficiente y sencillo.- Si escuchas
scroll:passive: true+requestAnimationFrame+ sólotransform. 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.