CSS

Animaciones vinculadas al Scroll: la API scroll-timeline para efectos parallax sin JS

Guía avanzada sobre scroll-timeline, scroll(), view() y animation-range para crear animaciones ligadas al scroll con CSS moderno y criterio de producción.

Contenido de la guía

CSS moderno · Scroll-driven animations · Arquitectura visual

La API scroll-timeline no existe para que hagas un parallax “más bonito”. Existe para cambiar la capa donde gobiernas el movimiento. En lugar de perseguir el scroll con listeners, cálculos manuales y estilos empujados desde JavaScript, puedes declarar una línea de tiempo vinculada al desplazamiento y dejar que el navegador sincronice la animación desde CSS.

🧭 scroll() y view() 🎞️ animation-timeline 🌐 Parallax sin JS 🧱 Progressive enhancement
De senior a senior: para quién es esta guía

Esta no es una guía básica para aprender animaciones CSS desde cero. Asumo que ya construiste efectos ligados al scroll, que ya usaste transform, opacity y probablemente requestAnimationFrame o IntersectionObserver para suavizar comportamientos que querías sincronizar con el desplazamiento.

También asumo que ya sentiste el costo mental de ese enfoque: leer posición, calcular progreso, empujar estilos y terminar tratando el scroll como una secuencia de parches de JavaScript en lugar de modelarlo como una timeline declarativa que el navegador puede entender por sí mismo.

El cambio importante de esta guía no es aprender una propiedad nueva. Es dejar de pensar el scroll como un evento que debes perseguir con JavaScript, y empezar a pensarlo como una línea de tiempo declarativa que CSS puede usar para gobernar el movimiento.

1. El dolor del scroll manual y el cambio de capa arquitectónica

Casi todos los que hemos construido efectos ligados al scroll cargamos la misma deuda técnica, aunque a veces esté escondida bajo una demo “que sí funciona”. Un listener de scroll, una lectura constante de scrollY, una fórmula casera para calcular progreso y una ráfaga de transform empujados desde JavaScript hacia el DOM. El resultado puede parecer vistoso durante unos minutos, pero la arquitectura que hay debajo suele ser frágil: demasiada responsabilidad en el hilo principal, demasiada lógica pegada a la frecuencia del desplazamiento y demasiadas oportunidades para introducir jitter, tearing visual o código imposible de mantener con calma seis meses después.

El problema real no es que ese enfoque esté “mal” en sentido moral. El problema es que nace de un modelo mental viejo. Durante años tratamos el scroll como un evento caótico que había que perseguir desde JavaScript: medir, recalcular, interpolar y empujar estilos manualmente como si el navegador no tuviera otra forma de relacionar movimiento y tiempo. En ese modelo, CSS solo recibe órdenes y JavaScript carga con la responsabilidad de convertir desplazamiento en animación.

Pero ahí está precisamente la ruptura que trae esta API. El scroll no es solo una fuente de eventos: también es una progresión. Tiene dirección, rango y avance. Va de un inicio a un final exactamente igual que una timeline de animación. Y en cuanto aceptas eso, aparece una idea mucho más potente: quizá no debías seguir persiguiendo el desplazamiento con JavaScript, sino declararlo como una línea de tiempo nativa que CSS puede usar directamente para gobernar el movimiento.

Esa es la promesa arquitectónica de scroll-timeline y de las scroll-driven animations: mover la sincronización del efecto hacia una capa más cercana al motor de renderizado. Ya no se trata de escuchar el scroll y reaccionar tarde. Se trata de declarar una relación entre desplazamiento y animación para que el navegador la procese de forma nativa, más fluida y con mucha menos fricción mental.

La mejora importante no es “hacer parallax sin JS” como truco. La mejora importante es dejar de perseguir el scroll desde JavaScript y empezar a declararlo como una timeline que CSS puede consumir directamente.

Lo que vas a leer a partir de aquí no busca enseñarte una propiedad aislada para sumar otro efecto visual a tu colección. Busca forzarte a cambiar de capa de pensamiento. Vamos a pasar del reflejo de “escuchar scroll y calcular cosas” a una arquitectura donde el movimiento se declara, el navegador sincroniza y JavaScript deja de actuar como intermediario innecesario entre el desplazamiento y la animación.

Advertencia de producción: esta guía no te va a vender fantasías. Las scroll-driven animations son una tecnología poderosa, pero su soporte todavía exige criterio y una estrategia de progressive enhancement.

Eso significa algo muy simple: si el navegador soporta animation-timeline, scroll() o view(), el usuario verá una experiencia más rica y más fluida. Si no la soporta, tu interfaz debe seguir siendo correcta, legible y usable sin romper layout, contenido ni navegación. Un efecto visual jamás debería secuestrar la estabilidad del producto.

Antes de entrar en sintaxis, demos y patrones, necesitamos fijar una base conceptual mínima: qué problema resuelve realmente esta familia de APIs y por qué su valor no está en “animar con scroll”, sino en cambiar quién gobierna esa relación dentro de la arquitectura de la interfaz.

2. Demo de dolor: el mismo requisito, dos arquitecturas distintas

Antes de bajar a scroll(), view(), rangos y sintaxis, conviene aterrizar el problema en algo mucho más cotidiano. Imagina un requisito completamente normal en una interfaz moderna: una tarjeta debe aparecer con un fade-in y una ligera traslación vertical a medida que entra en el viewport.

El efecto en sí no tiene nada de extraordinario. Lo interesante es la arquitectura con la que decides resolverlo. Puedes perseguir el desplazamiento desde JavaScript, leer posiciones, calcular progreso y empujar estilos manualmente. O puedes describir la relación entre visibilidad y movimiento como una timeline declarativa que el navegador ya sabe sincronizar desde CSS. El requisito es el mismo. La capa que gobierna el comportamiento cambia por completo.

El contraste importante no está en el efecto visual final. Está en quién carga con la responsabilidad de sincronizarlo: JavaScript desde el hilo principal o el navegador desde una timeline declarativa.

El enfoque imperativo: perseguir el scroll con JavaScript

Esta es la solución que muchos terminamos escribiendo por inercia. Escuchamos el scroll, medimos la posición del elemento, calculamos un progreso manual y traducimos ese valor a estilos en línea. Funciona, sí. Pero también deja ver por qué este patrón se vuelve caro tan rápido cuando la interfaz crece.

const card = document.querySelector(".tarjeta-animada");

function actualizarSegunScroll() {
  const rect = card.getBoundingClientRect();
  const viewportHeight = window.innerHeight;

  const start = viewportHeight;
  const end = viewportHeight * 0.3;
  const rawProgress = (start - rect.top) / (start - end);
  const progress = Math.max(0, Math.min(1, rawProgress));

  card.style.opacity = progress;
  card.style.transform = `translateY(${(1 - progress) * 40}px)`;
}

window.addEventListener("scroll", actualizarSegunScroll, { passive: true });
window.addEventListener("resize", actualizarSegunScroll);
actualizarSegunScroll();

A simple vista parece razonable. Pero si lo miras como arquitectura de interfaz, este patrón empieza a hacer demasiado trabajo en la capa equivocada.

Lo que haces realmente

  • Lees geometría del DOM durante el desplazamiento.
  • Calculas el progreso con matemática manual.
  • Empujas decisiones visuales desde JavaScript hacia estilos en línea.
  • Repites el patrón para cada nuevo efecto ligado al scroll.

Lo que eso te cuesta

  • Más trabajo en el hilo principal.
  • Más acoplamiento entre lógica y diseño visual.
  • Más fragilidad cuando cambian distancias, timings o layout.
  • Más probabilidad de jitter y desincronización perceptible.

El problema no es que este código sea “ilegal”. El problema es que obliga a JavaScript a actuar como intermediario permanente entre scroll, geometría y movimiento. En cuanto la experiencia depende de ese puente para verse fluida, el efecto deja de ser solo una animación y se convierte en una responsabilidad de sincronización en tiempo real.

El enfoque declarativo: CSS asume la sincronización

Ahora veamos exactamente el mismo requisito resuelto desde la nueva capa. Aquí ya no escuchamos el scroll ni calculamos progreso a mano. Declaramos una animación normal con @keyframes, pero cambiamos su fuente de tiempo: en lugar de depender del reloj, la ligamos a la visibilidad del elemento dentro del viewport.

@keyframes card-reveal {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.tarjeta-animada {
  animation-name: card-reveal;
  animation-duration: 1s; /* duración nominal */
  animation-timing-function: linear;
  animation-fill-mode: both;

  /* la animación ya no sigue segundos: sigue la visibilidad */
  animation-timeline: view();

  /* empieza al entrar y completa una parte de su recorrido visible */
  animation-range: entry 0% cover 30%;
}

El efecto visual es comparable. La diferencia importante está debajo:

Qué cambia cuando CSS gobierna la timeline

  • La sincronización deja de vivir en JavaScript: ya no lees scroll ni traduces valores manualmente a estilos.
  • La intención visual vuelve a CSS: distancia, opacidad, progresión y rango quedan donde pertenecen.
  • El navegador controla mejor el movimiento: la relación entre scroll y animación se expresa en una capa más cercana al renderizado.
  • El código gana legibilidad: en lugar de describir matemática reactiva, describes una política de movimiento.
Diagrama comparativo mostrando los saltos (jitter) de una animación controlada por eventos de JavaScript frente a la fluidez nativa de CSS usando animation-timeline.
Figura 1. Mientras el enfoque imperativo en JavaScript satura el hilo principal provocando jitter, la API nativa envía la línea de tiempo directamente al compositor de la GPU garantizando fluidez absoluta.
El ejemplo anterior ya deja ver la tesis completa de esta guía: el valor de scroll-timeline no está en ahorrarte unas líneas de JavaScript, sino en mover la sincronización del movimiento a una capa más coherente con la naturaleza del problema.

Con esa intuición ya clara, ahora sí tiene sentido entrar en la base conceptual de verdad: qué es exactamente una scroll-driven animation, qué tipos de timelines existen y por qué scroll() y view() no resuelven el mismo tipo de relación visual.

3. La formalización: el scroll como línea de tiempo, no como evento

Ya vimos el contraste práctico entre perseguir el desplazamiento con JavaScript y delegar la sincronización a CSS. Pero para dominar esta API de verdad, no basta con recordar que “se siente más limpia”. Hay que formalizar qué cambia exactamente en el modelo temporal de la animación.

En el modelo clásico, una animación CSS está gobernada por el reloj del documento. El navegador toma una duración, deja correr el tiempo y avanza el progreso desde 0 hasta 1. En las scroll-driven animations, ese reloj deja de ser la fuente principal de avance. La animación ya no progresa porque pasan segundos: progresa porque cambia una magnitud espacial vinculada al desplazamiento o a la visibilidad del elemento.

El cambio importante no es una propiedad nueva de CSS. Es sustituir la variable que gobierna el progreso de la animación.

Si lo escribimos con la mínima notación necesaria, una animación tradicional puede pensarse como una función cuyo progreso depende del tiempo:

$$ \text{progreso} = f(t) $$

Esa fórmula resume una intuición que damos por sentada desde hace años: si el reloj avanza, la animación avanza. Si declaras animation-duration: 2s, el navegador recorre los @keyframes siguiendo el paso del tiempo aunque el usuario no interactúe, cambie de pestaña o deje de desplazarse. El progreso visual queda desacoplado del estado espacial del documento.

Y eso justamente es lo que esta API rompe. La pregunta ya no es “¿cuántos segundos han pasado?”, sino “¿cuánto ha avanzado el contenedor?” o “¿qué tanto del elemento ha cruzado el viewport?”.

En una scroll-driven animation, el tiempo deja de ser la variable independiente principal. La progresión se vuelve espacial.

Eso nos deja con dos familias de formalización muy útiles para pensar esta API:

$$ \text{progreso}_{\text{scroll}} = f(\Delta y) \qquad\qquad \text{progreso}_{\text{view}} = f(I) $$

Aquí \(\Delta y\) representa el avance del desplazamiento sobre un eje vertical relevante para la timeline, mientras que \(I\) representa una medida de intersección o visibilidad del elemento respecto al viewport. No necesitas convertir esta notación en física dura. Solo necesitas retener la idea importante: el progreso ya no depende del reloj; depende de una magnitud espacial observable.

scroll()

  • La timeline depende del avance del contenedor de scroll.
  • La pregunta implícita es: cuánto se ha desplazado el eje.
  • Encaja bien cuando el movimiento debe seguir el progreso global del scroll.
  • Es natural para barras de progreso, indicadores y efectos ligados al recorrido del documento.

view()

  • La timeline depende de la visibilidad de un elemento dentro del viewport.
  • La pregunta implícita es: cuánto ha entrado, cruzado o salido el elemento.
  • Encaja bien para reveals, entradas, parallax localizado y efectos por aparición.
  • Es natural cuando la animación debe responder a la relación entre elemento y ventana visible.

Esta formalización parece pequeña, pero cambia por completo el modelo mental. En el enfoque clásico, la animación tiene una vida propia y el usuario solo la observa. En el enfoque ligado al scroll, la animación adquiere una relación directa con el estado espacial de la interfaz. El desplazamiento deja de ser un estímulo externo que JavaScript interpreta tarde, y pasa a ser la fuente declarativa del progreso.

Cuando el tiempo sale de la ecuación, la animación deja de “correr sola”. Se vuelve reversible, congelable y gobernada por el estado real del desplazamiento sin que tengas que programar esa lógica manualmente en JavaScript.

De ahí sale una consecuencia importantísima. Si el usuario deja de desplazarse, el progreso deja de avanzar. Si revierte el scroll, el progreso también revierte. Si el elemento apenas está entrando, la animación apenas está naciendo. No estás simulando esa relación con listeners, flags y cálculos manuales: estás declarando una correspondencia entre una línea de tiempo y una geometría observable del documento.

Dicho de otro modo: el navegador ya no necesita que le traduzcas el scroll a fotogramas. Le basta con que le declares qué tipo de timeline debe consumir.

Formalizarlo así no significa que el navegador esté “resolviendo matemáticas mágicas” por ti. Significa algo más sobrio y más útil: la plataforma ya trae un modelo nativo para mapear una progresión espacial a una progresión de animación, y tú puedes dejar de reconstruirlo manualmente desde JavaScript.

Con esta base ya clara, ahora sí podemos bajar del modelo conceptual al vocabulario operativo de la API: qué significan realmente animation-timeline, scroll(), view(), animation-range y cómo se conectan entre sí sin caer en confusión terminológica.

4. El vocabulario operativo: las piezas mínimas que sí gobiernan esta API

Después del cambio de modelo mental, toca ponerle nombres correctos a las piezas. Y aquí conviene ser muy disciplinado, porque una gran parte de la confusión alrededor de las scroll-driven animations no nace de la dificultad real de la API, sino de mezclar conceptos que pertenecen a capas distintas.

Una cosa es la animación que define qué cambia visualmente. Otra cosa es la timeline que define cómo progresa esa animación. Y otra, distinta todavía, es el rango dentro del cual decides recortar esa progresión. Si esas tres capas se te mezclan en la cabeza, todo parece arbitrario. Si las separas, la API se vuelve bastante más razonable.

No necesitas memorizar toda la especificación. Necesitas retener tres preguntas: qué se anima, qué gobierna el progreso y en qué tramo de esa timeline quieres que ocurra el efecto.

En la práctica, casi toda la arquitectura cotidiana de esta familia de APIs se construye combinando cuatro piezas: @keyframes, animation-timeline, una fuente de timeline como scroll() o view(), y un recorte de ejecución con animation-range. Lo demás importa, sí, pero entra después.

La gramática mínima

  • @keyframes define la transformación visual: opacidad, desplazamiento, escala, rotación, etc.
  • animation-timeline reemplaza el reloj clásico por una línea de tiempo vinculada al scroll o a la visibilidad.
  • scroll() usa el avance de un contenedor de scroll como fuente de progreso.
  • view() usa la relación entre un elemento y el viewport como fuente de progreso.
  • animation-range recorta en qué tramo de esa timeline quieres que viva realmente la animación.

Lo importante aquí es no leer estas propiedades como nombres sueltos. Léelas como una pequeña gramática. Primero declaras el movimiento. Después declaras quién gobierna el avance. Y finalmente declaras entre qué puntos de esa progresión quieres que ese movimiento ocurra.

Con esa separación ya clara, ahora sí tiene sentido bajar a las tres recetas mínimas que de verdad vas a reutilizar en producción.

@keyframes crecer-barra {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

.barra-progreso {
  transform-origin: left center;
  animation: crecer-barra linear both;
  animation-timeline: scroll(root block);
}

Receta 1: progreso atado al scroll del contenedor con scroll()

Esta es la forma más directa de decirle al navegador: “no quiero que esta animación avance con segundos; quiero que avance con el desplazamiento de un contenedor”. Si usas scroll(root block), la timeline queda ligada al scroll principal del documento sobre el eje vertical.

La lectura correcta es esta: arriba del todo, la animación está cerca del 0%. Al final del recorrido relevante, la animación está cerca del 100%. No estás midiendo visibilidad de un elemento concreto; estás usando el avance del scroll como un eje continuo de progreso.

Cuándo encaja mejor: barras de lectura, indicadores globales de progreso, efectos ligados al avance total del documento, cabeceras que responden al recorrido general de la página.
@keyframes card-reveal {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.tarjeta-reveal {
  animation: card-reveal linear both;
  animation-timeline: view(block);
  animation-range: entry 0% cover 30%;
}

Receta 2: progreso atado a la visibilidad del elemento con view()

Aquí la fuente de progreso ya no es el avance global del documento, sino la relación espacial entre un elemento y la ventana visible. view() convierte la visibilidad del propio nodo en una timeline consumible por CSS.

Ese detalle cambia completamente el tipo de problema que resuelves. Ya no preguntas cuánto ha avanzado la página, sino cuánto ha entrado, cruzado o salido este elemento del viewport. Por eso view() encaja tan bien para reveals, entradas suaves, parallax localizado y efectos de aparición.

El papel de animation-range aquí es fundamental. Sin ese recorte, la animación podría quedar distribuida sobre un tramo de visibilidad demasiado largo para el efecto que buscas. Con entry 0% cover 30%, le estás diciendo al navegador que complete el efecto pronto, durante la entrada inicial del elemento en pantalla.

Cuándo encaja mejor: tarjetas que aparecen, bloques que emergen al entrar al viewport, transiciones de secciones, efectos de entrada sin listeners ni IntersectionObserver.
@keyframes rotar-indicador {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(180deg);
  }
}

.galeria-horizontal {
  overflow-x: auto;
  scroll-timeline-name: --galeria-timeline;
  scroll-timeline-axis: inline;
}

.indicador-galeria {
  animation: rotar-indicador linear both;
  animation-timeline: --galeria-timeline;
}

Receta 3: desacoplar la fuente de progreso con una timeline nombrada

Las funciones anónimas como scroll() y view() son excelentes para muchos casos, pero no siempre bastan. En cuanto el nodo que se anima no coincide cómodamente con el contexto que genera el scroll, conviene nombrar explícitamente la timeline.

Ese patrón introduce una separación mucho más arquitectónica: un elemento declara la fuente de progreso y otro la consume. El contenedor que hace scroll expone una timeline con nombre. El elemento visual que debe reaccionar a esa progresión la inyecta mediante animation-timeline.

Una timeline nombrada es, en la práctica, una forma de desacoplar el lugar donde nace el desplazamiento del lugar donde consumes su progreso visual.

Este patrón es especialmente útil cuando trabajas con carruseles, regiones desplazables internas, indicadores remotos, decoraciones sincronizadas o cualquier caso en el que el sujeto animado no está cómodamente pegado al mismo nodo que define el scroll.

Cómo leer esta API sin confundirte

  • La animación responde a la pregunta: qué cambia visualmente.
  • La timeline responde a la pregunta: qué magnitud gobierna el progreso.
  • El rango responde a la pregunta: en qué tramo de esa progresión quiero que el efecto ocurra.

Si lo piensas así, la sintaxis deja de parecer rara. No estás decorando una animación con propiedades exóticas. Estás declarando una fuente de progreso distinta del tiempo y recortando el tramo donde quieres que esa fuente mueva tus @keyframes.

La trampa más fácil de cometer: el shorthand animation

La propiedad abreviada animation puede resetear subpropiedades relacionadas con la animación, y eso incluye piezas que no quieres perder cuando ya ligaste la animación a una timeline de scroll.

La regla práctica más segura es esta: primero define la animación base y después declara explícitamente animation-timeline y animation-range. Así reduces muchísimo la posibilidad de que el navegador vuelva silenciosamente al reloj clásico del documento.

/* Más seguro */
.elemento {
  animation: fade-up linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
}

/* Más propenso a errores si luego reescribes animation */
.elemento {
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
  animation: fade-up linear both;
}

Esta advertencia parece menor, pero en la práctica explica una parte enorme de los “no funciona” iniciales al experimentar con la API. No siempre estás rompiendo la timeline por una limitación del navegador. A veces la estás reseteando tú mismo al redeclarar animation en el orden equivocado.

Con este vocabulario ya asentado, el siguiente paso natural no es memorizar más sintaxis, sino aprender a decidir cuándo conviene usar scroll() y cuándo conviene usar view(). Ahí es donde realmente empieza el criterio de arquitectura visual.

5. Parallax nativo: cobrar la promesa sin perseguir el scroll con JavaScript

Llegó el momento de aplicar todo el modelo mental anterior al efecto que más deuda técnica ha generado en la historia del scroll visual: el parallax. Durante años se presentó como una simple floritura de interfaz, pero por debajo casi siempre escondía lo mismo: listeners, cálculos manuales, multiplicadores arbitrarios y una capa de JavaScript intentando traducir desplazamiento a profundidad.

La intuición del efecto es muy simple. Si dos planos visuales se mueven a velocidades distintas mientras el usuario avanza por la página, el ojo interpreta profundidad. El plano que se desplaza menos parece más lejano. El que se desplaza más parece más cercano. La ilusión no nace de una propiedad “mágica”, sino de una diferencia controlada de movimiento sobre una misma referencia de progreso.

El verdadero cambio no consiste en “hacer parallax con menos código”. Consiste en dejar de calcular velocidades manualmente en JavaScript y empezar a describir capas que comparten una misma timeline, pero recorren distancias visuales diferentes.

Esa idea es justo la que vuelve esta API tan elegante para este caso. Ya no necesitas que JavaScript mida el scroll, multiplique factores y empuje estilos en línea para cada capa. Puedes declarar una única fuente de progreso —el scroll principal del documento— y conectar varias animaciones independientes a esa misma timeline.

Antes de ver el código, conviene mirar el principio arquitectónico en forma visual: una sola línea de tiempo maestra, dos planos, dos recorridos distintos y una ilusión de profundidad producida por diferencias de desplazamiento, no por cálculos imperativos repartidos por el hilo principal.

Diagrama de parallax con dos capas y timeline de scroll
Figura 2. El parallax deja de ser una suma de multiplicadores manuales y pasa a modelarse como una composición de capas que consumen la misma timeline de scroll, pero recorren distancias distintas. El fondo se desplaza menos y por eso se percibe más lejano; el plano frontal recorre más distancia y s

Lo importante del diagrama no es solo que haya dos bloques moviéndose distinto. Lo importante es quién gobierna esa diferencia. En un enfoque clásico con JavaScript, tú tendrías que leer la posición del scroll, aplicar dos factores distintos —por ejemplo 0.2 para el fondo y 0.8 para el frente— y convertir ese cálculo en dos transform reactivos que deben mantenerse sincronizados con el desplazamiento real.

Aquí la arquitectura cambia por completo. La timeline deja de calcularse en JavaScript y pasa a existir como una fuente declarativa consumida directamente por CSS. El fondo y el frente no compiten por “escuchar” el scroll; simplemente responden a la misma línea de progreso con recorridos diferentes.

La idea arquitectónica: una sola timeline puede gobernar varias capas al mismo tiempo. La ilusión de profundidad nace de la diferencia entre sus trayectorias, no de tener múltiples listeners ni de reinyectar estilos cuadro por cuadro.
@keyframes parallax-fondo {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(24%);
  }
}

@keyframes parallax-frente {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-72%);
  }
}

.hero-fondo {
  animation: parallax-fondo linear both;
  animation-timeline: scroll(root block);
}

.hero-contenido {
  animation: parallax-frente linear both;
  animation-timeline: scroll(root block);
}

Cómo leer este código sin reducirlo a “otro snippet de efecto”

El código de arriba parece pequeño, pero la carga conceptual es fuerte. Ambas capas consumen exactamente la misma timeline: scroll(root block). Eso significa que el navegador toma el desplazamiento principal de la página como fuente continua de progreso y la ofrece a las dos animaciones al mismo tiempo.

La diferencia no está en la timeline. La diferencia está en el trayecto que cada plano recorre dentro de sus propios @keyframes. El fondo se desplaza poco. El frente se desplaza mucho más. Esa divergencia es la que produce la sensación de profundidad.

Qué está pasando realmente

  • La timeline es compartida: ambas capas leen el mismo progreso global del scroll.
  • La transformación no es compartida: cada capa interpreta ese progreso con un recorrido distinto.
  • La profundidad es una consecuencia perceptiva: menos desplazamiento se percibe como lejanía; más desplazamiento se percibe como cercanía.
  • La sincronización deja de ser una responsabilidad del hilo principal: el navegador gobierna el avance desde una capa más cercana al renderizado.

Este punto importa mucho porque corrige una mala costumbre heredada. En el enfoque imperativo, solemos pensar el parallax como una fórmula: scrollY * factor. En el enfoque declarativo, conviene pensar lo contrario: una misma progresión compartida por varias capas, donde cada una define cuánto de ese progreso convierte en desplazamiento visual.

Dicho de otra forma: ya no preguntas “¿qué cálculo hago para mover esta capa?”. La pregunta madura pasa a ser “¿qué recorrido visual quiero que haga esta capa sobre una timeline común?”.

En un parallax bien declarado, el scroll no se traduce manualmente a píxeles. El scroll se convierte en una timeline compartida y cada plano decide cómo recorrerla.

Dónde sí encaja este patrón y dónde conviene contenerse

Como casi todo efecto potente, el parallax mejora una interfaz solo cuando está subordinado a la jerarquía visual y al contenido. Su mejor uso no está en llenar toda la página de movimiento, sino en reforzar separación de planos, introducir una portada, dar más peso a un hero o producir una transición de entrada con sensación espacial.

  • Encaja bien en héroes, encabezados visuales, bloques editoriales destacados y composiciones donde la profundidad refuerza la lectura.
  • Encaja regular en páginas densas donde ya hay mucho texto, muchos elementos compitiendo o una jerarquía visual frágil.
  • Encaja mal cuando se usa como adorno permanente, cuando todo se mueve al mismo tiempo o cuando el efecto empieza a degradar legibilidad, foco o rendimiento percibido.

Esta advertencia importa porque una API nativa no convierte automáticamente cualquier decisión visual en una buena decisión de producto. Que ahora puedas hacer parallax sin listeners no significa que cualquier parallax sea una mejora.

Advertencia de diseño: más profundidad no significa mejor interfaz

El parallax tiene una tendencia natural a seducir demasiado al desarrollador. Como la técnica luce moderna y el código parece limpio, es fácil sobredosificarlo. Pero una interfaz no mejora porque más cosas se muevan: mejora cuando el movimiento refuerza orientación, jerarquía y ritmo visual.

Si el usuario empieza a percibir el efecto antes que el contenido, ya te pasaste. La profundidad debe acompañar la lectura, no competir con ella.

Hay además una ventaja práctica bastante elegante en este enfoque: al expresar el recorrido con porcentajes dentro de transform, el efecto se vuelve mucho más adaptable al tamaño del contenedor. No necesitas leer window.innerHeight, recalcular offsets en resize ni mantener a JavaScript corrigiendo dimensiones a cada cambio de layout.

Esa mejora no es trivial. Significa que parte de la responsividad del efecto vuelve a quedar donde debería: en la propia descripción visual del movimiento, no en una rutina reactiva obligada a reinterpretar la geometría del viewport todo el tiempo.

Con esto ya cobramos una parte importante de la promesa de la guía: sí, puedes construir un parallax convincente sin listeners, sin fórmulas manuales y sin usar JavaScript como puente permanente entre scroll y animación. Pero todavía falta la parte más delicada: aprender a controlar el tramo exacto donde la animación vive, para que el efecto no sea ni demasiado corto ni demasiado largo.

Ahí entra la siguiente pieza clave de esta API: animation-range, que es donde realmente empiezas a esculpir la progresión del movimiento en lugar de solo conectarla a una timeline.

6. El contraste decisivo: cuándo usar scroll() y cuándo usar view()

A esta altura ya sabes que una scroll-driven animation sustituye el reloj clásico por una línea de tiempo espacial. Pero todavía queda la confusión que más errores produce cuando alguien intenta usar esta API por primera vez con criterio real: pensar que scroll() y view() son casi lo mismo.

No lo son. Y la diferencia no es decorativa, sino arquitectónica. Ambas funciones producen una timeline, sí, pero no observan la misma magnitud ni responden a la misma pregunta. Si eliges mal, el efecto empieza demasiado pronto, se queda dormido cuando no debería o termina ligado a una progresión que no coincide con la promesa visual que querías construir.

scroll() mira el avance de un contenedor que se desplaza. view() mira la visibilidad de un elemento dentro del viewport. Una mide recorrido global; la otra mide intersección local.

Esa separación parece pequeña cuando la lees rápido, pero cambia por completo la forma de diseñar el efecto. En un caso, la animación vive atada al progreso general del scroll. En el otro, permanece latente hasta que el elemento entra realmente en escena.

Pregunta scroll() view()
¿Qué toma como fuente de progreso? El avance del scroll de un contenedor o del documento. La relación de visibilidad entre un elemento y el viewport.
¿Qué está observando realmente? Cuánto se ha recorrido un eje de desplazamiento. Cuánto ha entrado, cruzado o salido un elemento de la ventana visible.
¿Cuándo empieza a tener sentido? Desde el inicio mismo del recorrido del contenedor. Solo cuando el elemento empieza a interactuar con el viewport.
Tipo de progresión Global, continua y ligada al documento o a un scroller. Local, contextual y ligada a un nodo concreto.
Casos donde encaja mejor Barras de lectura, progreso de página, parallax global, cabeceras que reaccionan al avance del documento. Reveals, entradas, tarjetas que emergen, imágenes que se activan al aparecer, animaciones por sección.
Error típico al usarla mal Usarla para elementos que deberían animarse solo cuando aparecen en pantalla. Usarla cuando en realidad necesitas seguir el progreso completo del documento.

La tabla anterior no separa solo dos funciones. Separa dos modelos de activación. scroll() sirve cuando el usuario está recorriendo un sistema completo y quieres que el efecto responda a ese trayecto global. view() sirve cuando el efecto solo cobra sentido en el instante en que una pieza concreta entra, cruza o abandona la escena visible.

Diagrama comparativo entre scroll() y view(): una timeline ligada al avance global del documento frente a otra ligada a la visibilidad local de un elemento.
Figura 3. scroll() convierte el recorrido global del contenedor en una timeline continua; view() activa el progreso en función de la intersección real del elemento con el viewport.

La forma más limpia de fijar esta diferencia en la cabeza no es repetir definiciones, sino verla como dos curvas de activación distintas. Con scroll(), la animación existe desde el primer pixel recorrido del contenedor. Con view(), la animación permanece dormida hasta que el elemento entra realmente en el espacio visible.

Ese matiz cambia por completo la sensación del efecto. Una barra de lectura que depende de view() sería absurda, porque su misión no es reaccionar a un bloque aislado, sino al recorrido del documento. Del mismo modo, una tarjeta de reveal ligada a scroll() suele empezar a “gastarse” demasiado pronto, porque su animación progresa aunque el usuario todavía no haya llegado visualmente a esa zona.

No elijas entre scroll() y view() pensando en cuál “se ve mejor”. Elige preguntándote qué magnitud gobierna de verdad el efecto: el avance del recorrido o la aparición del elemento.

Un criterio práctico para no equivocarte

Si lo reduces a una pregunta operacional, la decisión se vuelve mucho más simple:

  • Usa scroll() cuando el efecto deba seguir el progreso general del documento o de un contenedor desplazable.
  • Usa view() cuando el efecto deba seguir la aparición o cruce de un elemento concreto dentro del viewport.
/* progreso global del documento */
.barra-lectura {
  animation: crecer-barra linear both;
  animation-timeline: scroll(root block);
}

/* aparición local de una tarjeta */
.tarjeta-reveal {
  animation: fade-up linear both;
  animation-timeline: view(block);
  animation-range: entry 0% cover 30%;
}

Si quieres que algo responda al viaje completo del usuario por la página, piensa en scroll(). Si quieres que algo despierte justo cuando entra en escena, piensa en view().

Con esta separación ya firme, la API deja de sentirse confusa. Ya no estás eligiendo entre dos funciones parecidas, sino entre dos fuentes de verdad distintas para el progreso de la animación. Y eso nos deja listos para la siguiente pieza crítica: aprender a recortar ese progreso con precisión usando animation-range, que es donde dejas de enlazar el scroll de forma bruta y empiezas a esculpir el tramo exacto donde el efecto debe vivir.

7. Trampas y bordes reales: donde la API se encuentra con el producto

Hasta aquí la API puede parecer elegantísima: declaras una timeline, conectas una animación y el navegador hace el resto. Pero la ingeniería real no se juega en el ejemplo perfecto, sino en el momento en que esta idea entra en contacto con layouts heredados, componentes reutilizables, soporte desigual de navegadores y decisiones visuales tomadas con prisa.

Las scroll-driven animations son potentes, pero no viven fuera del mundo. Se insertan en árboles DOM concretos, en sistemas de diseño concretos y en productos donde no basta con que “la demo funcione”. Por eso conviene mirar las trampas que más degradan una implementación cuando alguien adopta esta API por primera vez con entusiasmo, pero sin suficiente fricción arquitectónica.

La dificultad real de esta API no está en su sintaxis. Está en entender qué condiciones del layout, del soporte y del diseño deben cumplirse para que esa sintaxis tenga sentido en producción.

Trampa 1: asumir soporte universal y esconder contenido por defecto

El primer error serio no es de CSS, sino de estrategia. Ocurre cuando diseñas la experiencia como si la timeline existiera para todos los usuarios y conviertes la animación en condición de visibilidad del contenido.

El patrón peligroso suele verse así: dejas un bloque en opacity: 0, confías en que animation-timeline lo lleve hasta opacity: 1 y publicas la interfaz como si ese recorrido fuera una garantía de plataforma. En navegadores sin soporte suficiente, o en contextos donde la implementación todavía es parcial, el resultado puede ser desastroso: el contenido queda técnicamente presente en el DOM, pero visualmente secuestrado.

El problema real: no estás usando la animación como mejora; la estás usando como requisito funcional de legibilidad.

La salida madura consiste en invertir el supuesto: el contenido debe ser correcto, legible y usable incluso sin timeline declarativa. Luego, si el navegador soporta la API, enriqueces la experiencia. Nunca al revés.

Trampa 2: resetear la timeline sin darte cuenta con el shorthand animation

Esta es una de las trampas más pequeñas en apariencia y más frustrantes en depuración. El problema aparece cuando declaras correctamente una timeline y, unas líneas después, redeclaras animation por costumbre.

.tarjeta {
  animation-timeline: view();
  animation-range: entry 0% cover 30%;

  /* esta línea puede resetear subpropiedades relacionadas */
  animation: fade-in 1s linear both;
}

El síntoma típico es confuso: “la animación sí existe, pero ya no responde al scroll”. Y claro: visualmente parece que algo funciona, solo que la progresión volvió a depender del reloj clásico del documento.

La forma más segura de escribirlo en producción es esta: primero declaras la animación base, y después especificas explícitamente la fuente de progreso y su rango.

Trampa 3: usar scroll() cuando el efecto en realidad necesita view()

Después de aprender la sintaxis, mucha gente cae en un error más conceptual: escoger una timeline que no corresponde con la promesa visual del efecto. El caso clásico es usar scroll() para una animación de aparición local.

El problema no es que el CSS sea inválido. El problema es que scroll() mide el avance global del recorrido. Eso significa que, para cuando el usuario llega a cierta sección profunda del documento, la animación puede venir ya muy adelantada o incluso completamente “gastada”.

El usuario entonces no percibe una entrada. Percibe apenas el estado tardío de un efecto que empezó a progresar mucho antes de que ese elemento tuviera relevancia visual.

Si el efecto debe nacer cuando un bloque entra en escena, no necesitas progreso global. Necesitas intersección local. Eso es view().

Dicho sin rodeos: scroll() sirve para seguir el viaje del usuario por un recorrido; view() sirve para sincronizar el momento en que una pieza aparece dentro de ese viaje.

Trampa 4: declarar una scroll timeline sobre un contenedor que en realidad no scrollea

Otra fuente de confusión aparece cuando el CSS parece correcto, pero la timeline no progresa nunca. En muchos casos el problema no está en la animación, sino en el supuesto físico del contenedor.

Estás viendo solo el 60% del contenido. Hazte Premium para acceder a la guía completa.

Comunidad

Comentarios y valoraciones

No hay comentarios aún. ¡Sé el primero en opinar!