wandres.dev
SCROLL-DRIVEN IV · El fallback y Firefox

El estado final por defecto

La estrategia de mejora progresiva aplicada a las timelines de scroll: invertir el orden de escritura para que el peor caso sea el caso correcto.

⏱ 17 min

Toda la mejora progresiva de las animaciones dirigidas por scroll cabe en una frase: el CSS que se aplica siempre describe el resultado final, y la animación es una capa que añade el camino hasta él. Suena obvio y es lo contrario de como escribe animaciones casi todo el mundo, porque el hábito adquirido con @keyframes de tiempo es declarar el estado inicial en la regla base. Cambiar ese hábito es el noventa por ciento del trabajo.

🎯 Al terminar esta lección sabrás
  • Reescribir una animación de scroll para que su estado base sea el estado final.
  • Aprovechar los keyframes implícitos para no duplicar el estado final.
  • Aplicar la técnica a los tres patrones que más se rompen.
  • Verificar el resultado sin tener Firefox instalado.

La inversión

Compara las dos formas de escribir el mismo revelado. La primera es la que sale sola:

/* Estado inicial en la regla base */
.tarjeta {
  opacity: 0;
  translate: 0 2rem;
  animation: entrar linear both;
  animation-timeline: view();
  animation-range: entry;
}

@keyframes entrar {
  to { opacity: 1; translate: none; }
}

La segunda es la misma animación con el orden invertido:

/* Estado final en la regla base */
.tarjeta {
  opacity: 1;
  translate: none;
}

@keyframes entrar {
  from { opacity: 0; translate: 0 2rem; }
}

@supports (animation-timeline: view()) {
  .tarjeta {
    animation: entrar linear both;
    animation-timeline: view();
    animation-range: entry;
  }
}

En los motores con timelines, las dos producen exactamente el mismo resultado visual. En el motor sin timelines, la primera deja la tarjeta invisible para siempre y la segunda deja la tarjeta visible sin animación. Esa es toda la diferencia, y es la diferencia entre un sitio roto y un sitio con menos adornos.

Fíjate en dos detalles del segundo bloque. El @keyframes solo tiene from: el estado final no se escribe porque un keyframe que no menciona una propiedad usa el valor que el elemento tendría sin la animación. Eso no es un truco, es cómo funciona la resolución de propiedades ausentes en keyframes, y aquí paga doble: no duplicas el estado final y no puedes desincronizarlo.

El segundo detalle es más sutil: la regla base declara opacity: 1 y translate: none explícitamente, aunque sean los valores iniciales de esas propiedades. Podrías omitirlas y el resultado sería idéntico. Escribirlas es documentación ejecutable: dice al que lea el fichero dentro de dos años que esos valores son el estado deseado y no un descuido, y evita que alguien “limpie” el @keyframes moviendo el from a la regla base.

Los tres patrones que más se rompen

El revelado. Ya lo hemos visto y es el más frecuente. Regla base con el contenido visible, from con el estado invisible.

La barra de progreso. El estado base es la barra al cien por cien, porque transform sin animación es la identidad. Eso miente. La solución no es invertir los keyframes, porque una barra de progreso invertida seguiría mintiendo: es ocultar la barra cuando no hay progreso que enseñar.

.progreso {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 4px;
  background: var(--acento);
  transform-origin: left center;
  display: none;
}

@supports (animation-timeline: scroll()) {
  .progreso {
    display: block;
    animation: avanzar linear both;
    animation-timeline: scroll();
  }
}

Es el caso donde la mejora progresiva no consiste en degradar la animación sino en retirar el componente entero, porque el componente sin animación no tiene ningún significado. Reconocer esos casos ahorra intentos inútiles de degradar lo indegradable.

El paralaje. Una imagen con translateY(-12%) en la regla base queda descolocada dentro de su marco si la animación no corre, y según el recorte se ve una franja vacía. La versión correcta parte del centro:

.figura > img {
  block-size: 124%;
  object-fit: cover;
  translate: 0 -12%;   /* centrada en el marco, estado en reposo */
}

@keyframes paralaje {
  from { translate: 0 -24%; }
  to   { translate: 0 0%; }
}

@supports (animation-timeline: view()) {
  .figura > img {
    animation: paralaje linear both;
    animation-timeline: view();
    animation-range: cover;
  }
}

Sin la animación, la imagen queda centrada en su marco: exactamente lo que se vería en un sitio que no tuviera paralaje. Con animación, recorre de un extremo al otro pasando por esa posición central. El estado base es el punto medio del recorrido, que es la generalización correcta de “el estado base es el estado final” cuando la animación es simétrica.

La pregunta que decide el diseño del componente, no solo del CSS

Hay una pregunta que conviene hacerse antes de escribir el primer keyframe, y que casi nadie hace porque parece filosófica: si esta animación no ocurriera nunca, ¿el componente seguiría teniendo sentido? Las respuestas se reparten en tres grupos y cada uno lleva a una técnica distinta. Si la respuesta es sí, tal cual —un revelado, una cabecera que se compacta, un color que cambia— la técnica es el estado final por defecto y no hay nada más que decidir. Si la respuesta es sí, pero con otra forma —un paralaje, un elemento que recorre la pantalla— la técnica es el estado en reposo por defecto, normalmente el punto medio del recorrido. Y si la respuesta es no, el componente no significa nada sin la animación —una barra de progreso, un indicador de posición en un carrusel, un contador que avanza con el scroll— entonces no estás ante un problema de CSS sino de diseño de producto: ese componente es una capacidad opcional del sitio, y lo honesto es no mostrarlo donde no funciona, no fingir que funciona. Lo que nunca hay que hacer es el cuarto camino, el que sale solo: dejar el componente visible en un estado que transmite información falsa. Una barra de progreso llena en el primer píxel es peor que ninguna barra, porque una barra ausente no dice nada y una barra llena dice una mentira concreta. Este análisis, además, no depende del navegador: sirve igual para el caso en que la timeline está inactiva por razones de layout, que ya sabes que puede ocurrir en cualquier motor.

Verificarlo sin tener el navegador

No hace falta instalar Firefox para comprobar que el fallback es correcto, y de hecho conviene poder comprobarlo en el mismo navegador donde desarrollas. Hay dos técnicas y las dos son útiles.

La primera es comentar temporalmente la propiedad. Directa, y con el defecto de que se olvida descomentar.

La segunda, mejor, es simular la ausencia con una consulta imposible. Cambia la condición del @supports por algo que ningún motor entienda y todo el bloque de mejora desaparece:

/* Cambia esto ... */
@supports (animation-timeline: scroll()) { /* ... */ }

/* ... por esto para ver el fallback */
@supports (animation-timeline: sarasa()) { /* ... */ }

Con las herramientas de desarrollo abiertas puedes hacer el cambio en vivo sobre la hoja de estilos y ver la página degradada al instante, sin recargar y sin tocar el fichero. Es la comprobación que debería formar parte de la revisión de cualquier trabajo con animaciones de scroll.

Y una tercera comprobación que cubre el otro modo de fallo, el que @supports no detecta: haz la ventana muy alta. Si el documento deja de desbordar, la timeline de scroll() queda inactiva y ves exactamente el mismo estado que vería alguien sin soporte. Es la forma más rápida de descubrir que tu página depende de una timeline activa sin darte cuenta.

⚔️ Audita tu propio CSS

Coge cualquier animación de scroll que hayas escrito —o el ejemplo del nivel 16 que prefieras— y hazle las tres preguntas del análisis de arriba. Después reescríbela con el estado correcto por defecto y verifica el resultado con el truco del @supports imposible. Si alguna de tus animaciones cae en el tercer grupo, decide explícitamente si el componente se oculta o se sustituye por otra cosa; anotar esa decisión en un comentario del CSS vale más que el propio código.