wandres.dev
SCROLL-DRIVEN III · view() y los rangos

La view timeline: el elemento como reloj

Qué es una view progress timeline, quién es el sujeto y quién el scrollport, y en qué se diferencia de scroll() cuando parecen hacer lo mismo.

⏱ 17 min

scroll() mide el recorrido de una barra de desplazamiento y no sabe nada de tus elementos. Para el noventa por ciento de los efectos que la gente quiere hacer al scrollear —revelar una tarjeta cuando aparece, mover una imagen mientras cruza la pantalla, encoger algo justo antes de que se vaya— esa medida es inútil, porque el número que necesitas no es “cuánto se ha scrolleado la página” sino “cuánto ha avanzado este elemento concreto por delante de mis ojos”. view() produce exactamente ese número, y con él llega la parte más difícil de todo el sistema.

🎯 Al terminar esta lección sabrás
  • Identificar el sujeto, el scrollport y el rango de visibilidad de una view timeline.
  • Escribir una animación con view() y con view-timeline-name.
  • Explicar por qué la misma animación cambia de velocidad según el tamaño del elemento.
  • Elegir entre scroll() y view() con un criterio y no por costumbre.

Tres piezas: sujeto, scrollport, rango de visibilidad

Una view progress timeline se define sobre un elemento llamado sujeto. El progreso mide cuánto ha recorrido ese sujeto la ventana visible de su contenedor de scroll más cercano, que la especificación llama scrollport. Por defecto, el 0% es el instante justo antes de que el sujeto asome por un borde y el 100% el instante justo después de que desaparezca por el opuesto.

La tercera pieza es el rango de visibilidad, que por defecto coincide con el scrollport entero pero se puede recortar con view-timeline-inset. Es el rectángulo contra el que se mide la visibilidad, y separarlo conceptualmente del scrollport es lo que permite decir “considera que el elemento ya no es visible cuando le quedan cien píxeles para llegar al borde”.

La forma anónima es una función, igual que scroll():

@keyframes aparecer {
  from { opacity: 0; transform: translateY(2rem); }
  to   { opacity: 1; transform: none; }
}

.tarjeta {
  animation: aparecer linear both;
  animation-timeline: view();
}

La gramática es view( [ <axis> || <'view-timeline-inset'> ]? ). Fíjate en la diferencia con scroll(): no hay argumento de scroller. No hace falta, porque el sujeto ya determina cuál es su scrollport —el ancestro más cercano que scrollee en el eje pedido— y no tiene sentido preguntar por otro. Lo que sí se puede pasar es el eje, con los mismos cuatro valores de siempre, y el recorte del rango de visibilidad.

Y aquí está la diferencia estructural con scroll(), la que hay que grabar: la timeline de view() es la del propio elemento sobre el que se declara. animation-timeline: view() en .tarjeta significa “mi animación avanza según cruzo yo la pantalla”. Si quieres que un elemento se anime según cruza otro, view() no vale y necesitas nombres.

Nombres para separar sujeto y animado

Las propiedades son las simétricas de las de scroll: view-timeline-name, view-timeline-axis, view-timeline-inset, y el atajo view-timeline que combina las tres. El nombre es un <dashed-ident> y las reglas de visibilidad en el árbol son exactamente las mismas que vimos para las scroll timelines, timeline-scope incluido.

/* El sujeto publica su progreso de vista */
.hero-imagen {
  view-timeline: --hero block;
}

/* Otro elemento se engancha a el */
.hero-titulo {
  animation: desvanecer linear both;
  animation-timeline: --hero;
  animation-range: exit 0% exit 100%;
}

Ese patrón —el título se desvanece según la imagen sale de pantalla— es imposible con view() a secas, porque el título tendría su propia geometría y su propio recorrido. Con nombres, el sujeto y el animado son elementos distintos y el contrato es explícito.

Un detalle que la especificación deja claro y que ahorra sorpresas: si un mismo elemento define una scroll timeline y una view timeline con el mismo nombre, gana la scroll timeline. No es un caso rebuscado: aparece cuando alguien copia un bloque de reglas y se deja la mitad.

⚠️
Sigue sin haber Firefox

Todo lo de este nivel comparte el mismo estado de soporte que el anterior: Chromium desde la 115, Safari desde la 26, y Firefox no lo ha implementado. La consecuencia práctica para view() es más grave que para scroll(), porque el patrón estrella —revelar contenido al entrar en pantalla— tiene la tentación de empezar con opacity: 0. Si escribes eso sin protección, en Firefox el contenido no existe visualmente. Nunca escribas el estado inicial de un revelado fuera de un @supports.

La velocidad depende del tamaño, y eso no es un bug

Este es el punto donde la intuición falla y conviene detenerse.

Con scroll(), el recorrido de la timeline es siempre el mismo para todos los elementos de la página: el recorrido scrollable del documento. Con view(), cada elemento tiene su propia timeline y su propio recorrido, y ese recorrido depende de su altura sumada a la altura del scrollport. Un elemento de doscientos píxeles en un viewport de ochocientos tiene un recorrido de mil píxeles de scroll desde que asoma hasta que desaparece. Uno de mil doscientos píxeles tiene un recorrido de dos mil.

La consecuencia es que dos tarjetas de alturas distintas con la misma animación y el mismo rango se animan a velocidades distintas. La grande tarda más scroll en completar su animación. Esto no es un defecto: es la definición de la timeline. Pero explica el efecto desconcertante de una rejilla de tarjetas de contenido variable donde unas parecen ir más rápidas que otras.

Hay dos formas de manejarlo. La primera es aceptarlo y diseñar con ello: en un revelado corto, acotado a entry, la diferencia es imperceptible porque entry dura lo que mide el elemento y los elementos pequeños entran rápido, que es lo natural. La segunda es normalizar el rango a algo que no dependa de la altura del sujeto, y ahí entry-crossing es la herramienta, porque mide el cruce de un borde concreto en lugar de la visibilidad completa. Los seis rangos y sus diferencias son el contenido de la lección sobre el modelo de rangos, que es la que hay que leer despacio.

Un sujeto por timeline, y ese sujeto tiene que caber en el layout

La trampa que hace perder más tiempo con view() no está en la sintaxis sino en el layout. La timeline se calcula a partir de la caja principal del sujeto, y hay tres situaciones donde esa caja no es la que crees. La primera: un elemento con position: fixed no se mueve respecto al scrollport, así que su view timeline no progresa nunca; queda técnicamente activa y clavada en un valor. La segunda: un elemento cuyo scrollport no scrollea en el eje pedido —lo más común, un view() por defecto en eje de bloque dentro de un carrusel horizontal— produce una timeline inactiva, y ya sabes que eso significa que la animación no aplica ningún valor, en silencio. La tercera, la más sutil: si el sujeto es un elemento en línea que se fragmenta en varias líneas, la caja principal no es un rectángulo único y el resultado depende de la implementación. La regla que evita las tres es aburrida y funciona: el sujeto de una view timeline debe ser un elemento de bloque, en flujo, dentro de un contenedor que scrollee en el eje que has pedido. Cuando algo no progresa, antes de tocar el animation-range comprueba esas tres cosas, en ese orden. Y si necesitas animar algo fijo según el scroll, lo que quieres no es view() sino scroll(), porque lo que estás midiendo es el desplazamiento de la página, no el recorrido de un elemento.

Cuál de las dos

El criterio que decide es qué está midiendo el efecto, y se puede formular como una pregunta: si la página fuera el doble de larga, ¿debería cambiar la animación?

Si la respuesta es sí —la barra de progreso de lectura, un indicador de posición, un fondo que va cambiando de color a lo largo del documento— lo que mides es el documento y quieres scroll(). Si la respuesta es no —una tarjeta que aparece al entrar, una imagen con paralaje, un texto que se desliza mientras pasa por delante— lo que mides es el elemento y quieres view().

Hay un tercer caso que confunde y merece nombrarse: la sección fija que ocupa la pantalla mientras se scrollea por delante de ella, el patrón que en las librerías se llama pin. Eso no es ninguna de las dos cosas de forma pura: es un elemento con position: sticky cuyo contenedor tiene altura, y la timeline correcta es una view() sobre el contenedor, no sobre el elemento pegajoso. El sujeto es el que se mueve; el pegajoso, por definición, no.

Efecto Timeline Sujeto
Barra de progreso de lectura scroll(root)
Sombra en el borde de un panel scroll(self)
Revelado de una tarjeta al entrar view() La tarjeta
Paralaje de una imagen de fondo view() nombrada El contenedor de la imagen
Sección fija con contenido que avanza view() nombrada El contenedor alto, no el sticky
Progreso de un carrusel horizontal scroll(nearest inline)