wandres.dev
VIEW TRANSITIONS · ClientRouter y transiciones

Animaciones a medida y accesibilidad del movimiento

Bajar de las animaciones integradas a la View Transitions API del navegador: los pseudo-elementos que expone (view-transition, group, image-pair, old y new), cómo animarlos con keyframes de CSS apuntando a un transition:name, cómo construir un objeto de animación propio para transition:animate, y la responsabilidad ineludible de respetar prefers-reduced-motion para que el movimiento no dañe a quien lo padece.

⏱ 18 min

Las animaciones integradas —fade, slide— cubren la mayoría de los casos, pero llega un punto en que quieres algo que el catálogo no tiene: un giro, un zoom con rebote, una entrada que combine desplazamiento y opacidad con tu propia curva. Para eso hay que bajar un piso, hasta la View Transitions API que el navegador expone por debajo de todo lo que hemos visto. Astro no la oculta: te da los nombres, y el navegador te da unos pseudo-elementos que puedes animar con CSS como cualquier otra cosa. Pero este poder viene con una obligación que no es opcional ni negociable: el movimiento que deleita a la mayoría puede marear, desorientar o enfermar a quien tiene sensibilidad vestibular. Animar bien es, antes que nada, saber cuándo no animar.

🎯 Al terminar esta lección sabrás
  • Conocer los pseudo-elementos que expone la View Transitions API del navegador.
  • Animar un transition:name con @keyframes de CSS apuntando a ::view-transition-old y ::view-transition-new.
  • Construir un objeto de animación propio para pasarlo a transition:animate.
  • Respetar prefers-reduced-motion para no dañar a quien es sensible al movimiento.

Los pseudo-elementos que el navegador te presta

Cuando ocurre una view transition, el navegador hace algo ingenioso: toma una captura del estado viejo y del nuevo, y las organiza en un árbol de pseudo-elementos que existe solo durante la animación y que puedes estilar con CSS. En la raíz está ::view-transition, que cubre toda la página. Por cada view-transition-name —el que Astro asigna con transition:name— cuelga un ::view-transition-group(nombre), y dentro un ::view-transition-image-pair(nombre) que contiene las dos capturas: ::view-transition-old(nombre), la instantánea del elemento saliente, y ::view-transition-new(nombre), la del entrante. Animar la transición es, literalmente, animar esas dos capturas.

La palabra clave especial es root: ::view-transition-old(root) y ::view-transition-new(root) son las capturas de la página entera, la transición por defecto. Apuntando a ellas rediseñas el fundido global; apuntando a un nombre concreto, rediseñas el de ese elemento.

Animar con keyframes de CSS

El patrón es directo. Defines tus @keyframes y los aplicas al pseudo-elemento del nombre que quieras animar, en una hoja de estilos global —porque estos pseudo-elementos viven en el documento, no dentro del scope de un componente—. Supongamos que quieres que el contenido principal entre desde abajo con un desvanecido en lugar del fundido plano por defecto.

---
// src/pages/index.astro
---
<main transition:name="principal">
  <slot />
</main>

<style is:global>
  @keyframes entrar-desde-abajo {
    from { opacity: 0; transform: translateY(30px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  @keyframes salir-hacia-arriba {
    from { opacity: 1; transform: translateY(0); }
    to   { opacity: 0; transform: translateY(-30px); }
  }
  ::view-transition-new(principal) {
    animation: entrar-desde-abajo 0.35s ease-out;
  }
  ::view-transition-old(principal) {
    animation: salir-hacia-arriba 0.35s ease-in;
  }
</style>

Fíjate en la simetría: la captura vieja se va con una animación, la nueva llega con otra. Controlas cada mitad por separado, con la duración y la curva que decidas. Todo lo que sabes de @keyframes de CSS —retardos, rebotes, encadenados— se aplica aquí sin nada nuevo que aprender, porque estás animando elementos normales que el navegador ha fabricado para ti.

Un objeto de animación para transition:animate

Escribir CSS global funciona, pero Astro ofrece una vía más encapsulada y reutilizable: transition:animate acepta, además de las animaciones con nombre, un objeto que describe la animación por completo. Ese objeto tiene dos ramas —forwards, para el salto hacia delante, y backwards, para cuando el usuario pulsa atrás— y cada rama define cómo se anima la captura vieja (old) y la nueva (new): el nombre del keyframe, su duración, su curva y su modo de relleno.

// src/animaciones.ts
import type { TransitionDirectionalAnimations } from 'astro';

const comun = { duration: '0.3s', easing: 'ease-out', fillMode: 'both' };

export const zoom: TransitionDirectionalAnimations = {
  forwards: {
    old: { name: 'zoom-salir', ...comun },
    new: { name: 'zoom-entrar', ...comun },
  },
  backwards: {
    old: { name: 'zoom-salir', ...comun },
    new: { name: 'zoom-entrar', ...comun },
  },
};
---
import { zoom } from '../animaciones';
---
<section transition:animate={zoom}>
  <slot />
</section>

Los keyframes zoom-entrar y zoom-salir siguen definiéndose en CSS, pero ahora la animación es un valor de JavaScript que importas y aplicas donde quieras, con la ventaja de que distingue el sentido del viaje: puedes hacer que ir hacia delante y volver atrás se sientan como movimientos opuestos, igual que hace slide de fábrica.

La accesibilidad no es opcional: prefers-reduced-motion

Aquí cambia el tono, porque cambia lo que está en juego. Para una parte real de las personas, el movimiento en pantalla no es un adorno agradable sino un disparador de mareo, náusea o desorientación —es el trastorno vestibular, y es tan real como la fotosensibilidad—. Los sistemas operativos permiten a esas personas pedir que se reduzca el movimiento, y el navegador expone esa petición como la media query prefers-reduced-motion. Ignorarla no es un descuido estético: es construir algo que puede enfermar a quien lo usa.

La buena noticia es que las animaciones integradas de Astro ya respetan esta preferencia: cuando el usuario pide menos movimiento, fade y slide recortan su animación por ti. La responsabilidad es tuya en cuanto escribes CSS propio. Toda animación que definas a mano debe envolverse de modo que solo corra cuando el usuario no ha pedido reducirla.

<style is:global>
  @media (prefers-reduced-motion: no-preference) {
    ::view-transition-new(principal) {
      animation: entrar-desde-abajo 0.35s ease-out;
    }
    ::view-transition-old(principal) {
      animation: salir-hacia-arriba 0.35s ease-in;
    }
  }
</style>

Al condicionar la animación a no-preference, quien no ha expresado preferencia disfruta del movimiento y quien pidió reducirlo recibe una transición sobria —un cambio limpio, sin desplazamientos—. También puedes tomar el camino inverso: definir la animación siempre y anularla dentro de @media (prefers-reduced-motion: reduce). Cualquiera de las dos formas cumple; lo que no cumple es no hacer nada.

⚠️
Movimiento por defecto no, movimiento por elección sí

La postura correcta invierte la intuición del que solo piensa en el efecto: el movimiento intenso no debe ser lo que todos reciben salvo que se protejan, sino lo que reciben quienes no han pedido evitarlo. Envuelve toda animación propia en prefers-reduced-motion: no-preference, o anúlala bajo reduce. Y no te limites a las view transitions: la misma disciplina vale para cualquier animación de tu sitio. Un solo desplazamiento brusco no controlado basta para arruinarle la visita a alguien.

flowchart TD
A[view-transition raiz de la pagina] --> B[group por cada transition name]
B --> C[image-pair del elemento]
C --> D[old captura de la vieja]
C --> E[new captura de la nueva]
D --> F[animas old con keyframes de CSS]
E --> G[animas new con keyframes de CSS]
F --> H[todo bajo prefers-reduced-motion]
G --> H
style A fill:#89b4fa,color:#11111b
style H fill:#a6e3a1,color:#11111b
🌳

Un arbol de capturas

El navegador expone view-transition, group, image-pair, old y new: pseudo-elementos que existen durante la animacion.

🎨

Keyframes normales

Apunta con CSS global a old y new de un nombre y animalos con keyframes: no hay API nueva que aprender.

📦

Objeto reutilizable

transition:animate acepta un objeto con forwards y backwards para encapsular una animacion con sentido de direccion.

Respeta la preferencia

prefers-reduced-motion no es opcional. Los integrados ya la respetan; tu CSS propio debes condicionarlo tu.

El movimiento es un mensaje, y todo mensaje puede herir a quien no puede recibirlo

Cerrar este nivel con la accesibilidad no es un apéndice moral pegado al final: es el punto donde las view transitions revelan su naturaleza más honda. Una animación nunca es neutral; siempre comunica algo —esto continúa aquello, esto entra, esto se va— y comunicar es un acto que presupone a alguien capaz de recibir el mensaje en las condiciones en que lo emites. Cuando das por sentado que todo el mundo puede procesar un desplazamiento veloz sin coste, estás construyendo para un cuerpo imaginario que no es universal, y excluyendo silenciosamente a quien no lo tiene: la persona cuyo sistema vestibular convierte tu elegante zoom en un vértigo, la que cierra la pestaña no porque no le gustara tu sitio sino porque le hizo daño. La API que acabamos de aprender es poderosa precisamente porque el navegador te cede el control de un movimiento que antes gestionaba él; y todo poder cedido llega con la responsabilidad de ejercerlo sin dañar. Que prefers-reduced-motion exista es la web haciendo lo que mejor sabe: darle a cada persona una voz para declarar cómo necesita que la traten, y pedirle al constructor que escuche. Respetarla no es hacer tu sitio “más accesible” como quien añade una rampa al final; es reconocer que el sitio nunca estuvo terminado mientras solo funcionara para los cuerpos que se parecen al tuyo. La maestría técnica que has ganado en este nivel —interceptar la navegación, emparejar elementos, animar capturas— solo se convierte en oficio cuando la gobierna esta pregunta previa a cualquier efecto: ¿y si quien lo mira no puede permitirse el movimiento que yo doy por descontado? Animar para todos empieza por animar también para quien te pide que no lo hagas.

⚔️ Anima con criterio y con cuidado
  1. Dale a tu contenido principal un transition:name y anímalo con @keyframes propios apuntando a ::view-transition-old y ::view-transition-new en un <style is:global>.
  2. Extrae esa animación a un objeto con forwards y backwards y aplícala con transition:animate, haciendo que ir y volver se sientan opuestos.
  3. Envuelve tus keyframes en @media (prefers-reduced-motion: no-preference) y verifica, activando la reducción de movimiento en tu sistema, que la transición se vuelve sobria.
  4. Audita el resto de tu sitio en busca de cualquier animación no protegida y aplícale la misma condición, para que ninguna se dispare contra quien pidió calma.