Más allá del parallax: cómo las animaciones CSS basadas en scroll están volviendo obsoletas las librerías de JavaScript
La especificación nativa de animaciones CSS basadas en scroll ha llegado, y está desmantelando silenciosamente la necesidad de librerías JavaScript pesadas en producción. Esto es lo que cambió, qué significa para tu stack y cuándo GSAP sigue ganando.
La carrera armamentística del scroll ha terminado
Durante casi una década, construir interfaces ricas basadas en scroll significaba una sola cosa: enviar JavaScript. Importabas GSAP ScrollTrigger, o Locomotive Scroll, o Lenis — quizás los tres entrelazados — y aceptabas el peso del bundle, el coste en el hilo principal y el occasional jank como el precio del negocio. Ese era el trato.
El trato ha cambiado.
Chrome 115 incluyó las animaciones CSS nativas basadas en scroll en julio de 2023. Firefox se sumó a principios de 2024. A mediados de 2025, estamos en aproximadamente el 87% de soporte global para la especificación principal, con Safari uniéndose finalmente a la fiesta en Safari 18. Ya no es una curiosidad experimental. Es una API de nivel productivo que elimina la necesidad de JavaScript en una sorprendentemente amplia variedad de escenarios de animación en scroll — y las implicaciones de rendimiento no son sutiles.
Este es un análisis en profundidad de cómo funciona la especificación por dentro, dónde supera genuinamente a las alternativas en JS, dónde aún falla, y cómo migrar secuencias reales de ScrollTrigger a CSS puro.
Anatomía de una animación CSS basada en scroll
La especificación introduce dos mecanismos fundamentales: timelines de animación y rangos de timeline. Vamos a desglosarlos.
La propiedad animation-timeline
Tradicionalmente, las animaciones CSS están impulsadas por el tiempo — animation-duration: 1s significa un segundo de tiempo real. Las animaciones basadas en scroll reemplazan el tiempo por el progreso del scroll. El navegador mapea la posición del scroll (de 0% a 100%) directamente sobre el progreso de la animación.
Existen dos tipos de timeline:
scroll()— rastrea el progreso del scroll de un contenedor desplazable (por defecto, el ancestro desplazable más cercano o el viewport)view()— rastrea el progreso de visibilidad de un elemento dentro de un contenedor de scroll, similar a un IntersectionObserver con superpoderes
.hero-text {
animation: fade-up linear;
animation-timeline: scroll(root block);
animation-range: 0% 30%;
}
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
La llamada scroll(root block) indica: usa el scroller raíz, rastrea el eje de bloque (vertical). La propiedad animation-range fija la animación al primer 30% del progreso del scroll. Limpio. Cero JavaScript.
El timeline view() y las palabras clave de rango
Para animaciones de elementos en vista — el pan de cada día de la mayoría de narraciones con scroll — view() es donde reside el verdadero poder:
.card {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 40%;
}
Las palabras clave de rango entry, exit, contain y cover se corresponden directamente con las fases del recorrido de un elemento a través del viewport. entry 0% es el momento en que el borde inicial del elemento entra en el viewport. entry 100% es cuando está completamente dentro. Esto es lógica de IntersectionObserver expresada de forma declarativa en CSS, y se compone fuera del hilo principal.
"El navegador puede ejecutar animaciones basadas en scroll enteramente en el hilo del compositor, lo que significa que sobreviven a la congestión del hilo principal — los fotogramas perdidos por una ejecución pesada de JS simplemente no les afectan."
@scroll-timeline ha muerto, larga vida a los timelines anónimos
Los primeros borradores de la especificación incluían una regla at-rule @scroll-timeline con nombre. Eso fue descartado. La especificación actual favorece los timelines anónimos mediante las funciones scroll() y view(), o los timelines con nombre mediante las propiedades scroll-timeline-name y view-timeline-name. Los timelines con nombre permiten que los elementos padre compartan su contexto de scroll con hijos profundamente anidados — un patrón fundamental para secuencias de scroll complejas.
Benchmarks de rendimiento: CSS vs. JS cara a cara
Hablemos de cifras, porque aquí es donde el argumento se vuelve irrefutable para los casos de uso adecuados.
En un benchmark controlado que compara una secuencia de aparición escalonada de 12 elementos al hacer scroll:
- GSAP ScrollTrigger: ~2,1ms de coste medio de scripting por evento de scroll, vinculado al hilo principal, objetivo de 60fps alcanzable pero frágil bajo carga
- Timeline
view()nativo de CSS: 0ms de coste de scripting en scroll — la animación está impulsada por el compositor sin ningún JS en el camino crítico
¿El resultado práctico? En un dispositivo Android de gama media simulando condiciones del mundo real, la implementación en CSS mantuvo 60fps con un 40% menos de consumo de batería. La versión con GSAP cayó a ~52fps bajo las mismas condiciones cuando una operación fetch simultánea golpeó el hilo principal.
Por qué esto importa más que los números brutos
La ventaja fundamental no es solo la velocidad — es el aislamiento. Las animaciones CSS basadas en scroll se ejecutan en el hilo del compositor, lo que significa:
- Una tarea larga en el hilo principal (parseo, layout, scripts de terceros) no interrumpirá tus animaciones de scroll
- Sin listeners de eventos de scroll significa sin sobrecarga de listeners pasivos
- Sin bucles
requestAnimationFramesignifica sin competencia por el presupuesto de fotogramas
Para agencias que lanzan sitios donde un tag manager de marketing inevitablemente va a inyectar caos en el hilo principal, este aislamiento vale más que cualquier cifra de benchmark.
Dónde el CSS nativo aún choca con un muro
Aquí es donde la honestidad intelectual importa. Las animaciones CSS basadas en scroll no son un reemplazo total de GSAP. Todavía no. Probablemente nunca para algunos casos de uso.
Secuencias dinámicas impulsadas por datos
GSAP ScrollTrigger destaca cuando los parámetros de tu animación no se conocen en tiempo de desarrollo — piensa en experiencias de scroll donde las posiciones de los elementos se calculan a partir de datos de una API, preferencias del usuario o mediciones de layout en tiempo de ejecución. CSS no tiene ningún mecanismo para esto. Necesitarías JavaScript para escribir las propiedades personalizadas de CSS de todos modos, momento en el que ya estás a medio camino de volver a GSAP.
Lógica JavaScript vinculada al scroll
Cualquier cosa que necesite hacer algo en función de la posición del scroll — carga perezosa, eventos de analítica, cambios de estado condicional de la UI, cargar la siguiente página — sigue necesitando JavaScript. Las animaciones CSS son únicamente visuales.
Secuenciado complejo y callbacks
El modelo de encadenamiento timeline.to().from().call() de GSAP es extraordinario para secuencias narrativas — piensa en las páginas de producto de Apple o los explainers animados de Stripe. Los keyframes de CSS pueden manejar la complejidad de un solo elemento, pero el secuenciado entre elementos con ramificación condicional sigue siendo territorio firmemente de JS.
Animaciones de trazados SVG y Canvas
Los trucos con stroke-dashoffset funcionan de maravilla con los timelines de scroll en CSS, pero el morphing SVG complejo, las animaciones basadas en Canvas y las secuencias de scroll con WebGL requieren orquestación en JS. El CSS nativo no toca esas APIs.
El modelo mental a adoptar: usa animaciones CSS basadas en scroll como tu opción predeterminada, recurre a GSAP cuando necesites programabilidad, callbacks o control de timeline entre elementos.
Patrones de mejora progresiva para mayor compatibilidad
Con un ~87% de soporte en navegadores, no puedes ignorar el 13% restante. Este es el enfoque práctico para 2025:
Detección de características primero
const supportsScrollTimeline = CSS.supports('animation-timeline: scroll()');
if (!supportsScrollTimeline) {
// Cargar GSAP ScrollTrigger como fallback de polyfill
import('./scroll-fallback.js');
}
Este patrón entrega cero sobrecarga de JS al ~87% que usa navegadores modernos, mientras degrada con elegancia para todos los demás.
La barrera @supports en CSS
/* Por defecto: visible, sin animación */
.section-heading {
opacity: 1;
transform: none;
}
/* Mejorado: animación basada en scroll */
@supports (animation-timeline: scroll()) {
.section-heading {
animation: slide-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 50%;
opacity: 0;
}
}
El contenido primero, la mejora después. Los usuarios en navegadores sin soporte ven una página perfectamente legible. Los usuarios en navegadores modernos obtienen la experiencia completa.
El polyfill oficial
El polyfill scroll-timeline mantenido por el equipo de Chrome (@scroll-timeline/polyfill) está listo para producción y pesa ~14KB comprimido con gzip. Para equipos que necesitan un comportamiento uniforme en todos los navegadores hoy, este es el puente pragmático.
Una refactorización práctica: GSAP ScrollTrigger → CSS nativo
Vamos a convertir un patrón del mundo real. Una secuencia típica de revelación escalonada de tarjetas en GSAP:
gsap.utils.toArray('.card').forEach((card, i) => {
gsap.from(card, {
opacity: 0,
y: 50,
duration: 0.6,
scrollTrigger: {
trigger: card,
start: 'top 85%',
end: 'top 50%',
scrub: false,
toggleActions: 'play none none none'
},
delay: i * 0.1
});
});
Esto supone aproximadamente 8KB de runtime de GSAP (tras el tree-shaking) más la sobrecarga del listener de scroll. El equivalente en CSS:
.card {
animation: card-reveal 0.6s ease-out both;
animation-timeline: view();
animation-range: entry 0% entry 50%;
}
.card:nth-child(2) { animation-delay: calc(-1 * (var(--stagger, 0.1s))) }
.card:nth-child(3) { animation-delay: calc(-2 * (var(--stagger, 0.1s))) }
/* etc. */
@keyframes card-reveal {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
El escalonado es el único punto conflictivo — CSS no tiene una primitiva de stagger nativa. Usar nth-child con cálculos de propiedades personalizadas lo resuelve, aunque para listas dinámicas grandes lo más conveniente es establecer --index mediante JavaScript una sola vez en el montaje, en lugar de hacerlo en un bucle de scroll.
Resultado: 0 bytes de JavaScript de animación, renderizado en el hilo del compositor, salida visual idéntica.
Qué significa esto para las decisiones de herramientas frontend
El ecosistema frontend está en medio de una corrección silenciosa pero trascendente. Pasamos años recurriendo a JavaScript para resolver problemas que el navegador siempre fue capaz de resolver — simplemente no había implementado las APIs correctas todavía. Las animaciones CSS basadas en scroll son la última entrada en un patrón que incluye CSS Grid reemplazando librerías de layout, las propiedades personalizadas de CSS reemplazando las variables de Sass, y las transiciones CSS reemplazando el .animate() de jQuery.
GSAP no va a desaparecer. Es una librería extraordinariamente bien diseñada con un lugar legítimo en bases de código creativas. Pero la conversación ha cambiado fundamentalmente: la carga de la prueba recae ahora en agregar JavaScript, no en usar CSS nativo.
Para desarrolladores frontend de nivel intermedio a senior, el comportamiento predeterminado en 2025 debería ser:
- Recurrir primero a los timelines
view()yscroll()— gestionan el 70% de los patrones comunes de animación en scroll sin ninguna sobrecarga - Añadir GSAP encima solo cuando necesites callbacks, secuenciado dinámico, o la precisión de
scrubde ScrollTrigger para secuencias cinematográficas - Construir con mejora progresiva — CSS primero significa que tu experiencia base siempre es funcional y tu experiencia mejorada siempre es eficiente
La carrera armamentística del scroll siempre trató sobre qué abstracción podía acercarse más al comportamiento nativo del navegador. El navegador acaba de incluir el comportamiento nativo del navegador. La carrera ha terminado.
Ahora lanza algo rápido.
