Saltar al contenido
view-transitions.css · devschool

Transiciones de vista en CSS: View Transitions API

Lección 37 de 50 · 13 min de lectura · Actualizado el

En esta lección
  1. La idea: dos fotos y una animación
  2. Transiciones entre páginas con @view-transition
  3. Dar nombre a los elementos: view-transition-name
  4. Los pseudoelementos de la transición
  5. Personalizar la animación
  6. Transiciones dentro de la misma página: startViewTransition()
  7. Respetar prefers-reduced-motion
  8. ¿Y las animaciones ligadas al scroll?
  9. Cuándo usar transiciones de vista
  10. Errores frecuentes
  11. Resumen

Cuando pulsas un enlace, el navegador borra la página actual y pinta la nueva de golpe. En una app del móvil, en cambio, las pantallas se deslizan, las fotos crecen hasta ocupar la pantalla y los elementos se recolocan con suavidad. La View Transitions API (API de transiciones de vista) trae ese efecto a la web: el navegador hace una foto del estado anterior, otra del nuevo y anima el paso de una a otra. En esta lección aprenderás a crear transiciones entre páginas solo con CSS, a animar cambios dentro de una misma página con document.startViewTransition(), a dar nombre a los elementos que quieres que “viajen” y a personalizar o desactivar la animación.

La idea: dos fotos y una animación

Las transiciones normales animan una propiedad de un elemento (un color, un tamaño). Pero hay cambios que no se pueden expresar así: pasar de una página a otra, cambiar una lista a cuadrícula, reordenar productos o abrir el detalle de una ruta. El elemento cambia de sitio en el HTML, o directamente es otro documento.

Una transición de vista funciona de otra manera:

  1. El navegador hace una captura del estado actual (el estado “viejo”).
  2. Tú cambias la página: navegas a otra, cambias clases, añades o quitas elementos.
  3. El navegador toma el estado nuevo, que sigue vivo (no es una foto).
  4. Coloca ambos en una capa por encima de todo y anima de uno al otro. Por defecto, un fundido cruzado: el viejo desaparece mientras el nuevo aparece.

Lo bueno de este enfoque es que tú no calculas posiciones ni tamaños intermedios. Te limitas a decir “cambia esto” y el navegador se encarga de la animación.

Hay dos tipos:

TipoCuándoCómo se activa
Entre documentos (cross-document)Al navegar de una página a otra del mismo sitioSolo CSS: @view-transition
En el mismo documento (same-document)Al cambiar el DOM sin recargarJavaScript: document.startViewTransition()

Transiciones entre páginas con @view-transition

Imagina la web de un rocódromo con páginas index.html, horarios.html y tarifas.html. Para que al pulsar un enlace haya un fundido en vez de un salto brusco, añade esto a la hoja de estilos que comparten todas las páginas:

@view-transition {
  navigation: auto;
}

Y ya está. No hace falta JavaScript. La regla @view-transition con navigation: auto le dice al navegador: “cuando el usuario navegue entre páginas de este sitio, haz una transición”.

Condiciones para que funcione:

  • La regla tiene que estar en las dos páginas: la de origen y la de destino. Si la pones en una sola hoja de estilos común, se cumple solo.
  • Las dos páginas deben ser del mismo origen (mismo dominio, protocolo y puerto). Un enlace a otra web no tiene transición.
  • Se activa al seguir enlaces y al usar los botones de atrás y adelante del navegador.

Nota: las transiciones entre documentos funcionan en Chrome, Edge y Safari recientes. En un navegador sin soporte, la regla se ignora y la navegación es la de siempre. Es una mejora progresiva: quien no la tiene no pierde nada.

El fundido por defecto dura unos 250 ms. Enseguida verás cómo cambiarlo.

Dar nombre a los elementos: view-transition-name

Con lo anterior, la página entera se funde como un solo bloque. Pero lo vistoso es que ciertos elementos viajen de su posición antigua a la nueva: la foto pequeña de una ruta en el listado crece hasta convertirse en la foto grande de la página de detalle.

Para eso le das al elemento un nombre de transición con view-transition-name, el mismo en el estado viejo y en el nuevo:

/* En rutas.html: la miniatura de la ruta del Cares */
.ruta-cares .miniatura {
  view-transition-name: foto-cares;
}

/* En cares.html: la foto grande de la cabecera */
.cabecera-ruta img {
  view-transition-name: foto-cares;
}

Al navegar, el navegador ve que en las dos páginas hay un elemento llamado foto-cares y, en lugar de fundirlo con el resto, anima su posición y su tamaño de una a otra. El resto de la página sigue con el fundido normal.

Reglas del nombre:

  • Es un identificador inventado por ti, sin comillas: foto-cares, cabecera, titulo-principal.
  • Tiene que ser único en la página en el momento de la captura. Si dos elementos visibles comparten nombre, el navegador cancela la transición y muestra un error en la consola. Por eso en un listado cada tarjeta necesita su propio nombre.
  • none (el valor por defecto) significa “sin nombre propio”: el elemento forma parte de la captura general.
  • El nombre root está reservado: es el de la página entera, que ya lo tiene asignado.

Un uso muy práctico es nombrar la cabecera para que no se mueva al cambiar de página, mientras el contenido se funde:

.cabecera-sitio {
  view-transition-name: cabecera;
}

Como la cabecera está en el mismo sitio en las dos páginas, su animación no tiene nada que mover y se queda quieta. El efecto es el de una app: el menú permanece y solo cambia el contenido.

Nombres para muchos elementos

En una lista generada con muchos elementos, escribir un nombre para cada uno en el CSS no es práctico. Se suele poner en línea desde el HTML que genera el servidor o la plantilla:

<li style="view-transition-name: ruta-12">Ruta del Cares</li>
<li style="view-transition-name: ruta-13">Lagos de Covadonga</li>

Los navegadores más recientes también aceptan view-transition-name: match-element, que asigna un nombre automático a cada elemento según su identidad en el DOM. Solo sirve dentro de la misma página (en otro documento los elementos son distintos), y aún no está en todos los navegadores.

Los pseudoelementos de la transición

Mientras dura la transición, el navegador construye un pequeño árbol de pseudoelementos encima de la página. Conocerlo es la clave para personalizar la animación:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)
└─ ::view-transition-group(foto-cares)
   └─ ::view-transition-image-pair(foto-cares)
      ├─ ::view-transition-old(foto-cares)
      └─ ::view-transition-new(foto-cares)
PseudoelementoQué representaAnimación por defecto
::view-transitionLa capa que cubre toda la ventanaNinguna
::view-transition-group(nombre)La caja de un elemento con nombreMueve y redimensiona de la posición vieja a la nueva
::view-transition-image-pair(nombre)Agrupa las dos imágenesNinguna
::view-transition-old(nombre)La captura del estado viejoSe desvanece (de opacidad 1 a 0)
::view-transition-new(nombre)El estado nuevo, vivoAparece (de opacidad 0 a 1)

Entre paréntesis va el nombre del grupo. Puedes usar * para referirte a todos los grupos a la vez.

Personalizar la animación

Los pseudoelementos se animan con las mismas propiedades de siempre: animation-duration, animation-timing-function y @keyframes. Si no las dominas aún, repasa animaciones.

Cambiar la duración

::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
  animation-duration: 0.4s;
}

Un deslizamiento en lugar de un fundido

Para que la página vieja salga por la izquierda y la nueva entre por la derecha, como al pasar pantallas en el móvil:

@keyframes salir-izquierda {
  to { translate: -30% 0; opacity: 0; }
}

@keyframes entrar-derecha {
  from { translate: 30% 0; opacity: 0; }
}

::view-transition-old(root) {
  animation: 0.3s ease-in both salir-izquierda;
}

::view-transition-new(root) {
  animation: 0.3s ease-out both entrar-derecha;
}

Solo has tocado root, así que los elementos con nombre propio (la cabecera, la foto) siguen con su animación de grupo.

Que un elemento no se funda

Cuando un elemento con nombre cambia de proporción (una miniatura cuadrada que pasa a ser una foto panorámica), el fundido entre la versión vieja y la nueva puede verse borroso. A veces queda mejor quitar el fundido de ese elemento y dejar solo el movimiento:

::view-transition-old(foto-cares) {
  animation: none;
  opacity: 0;
}

::view-transition-new(foto-cares) {
  animation: none;
}

Estilos para varios grupos: view-transition-class

Si quieres el mismo efecto en todas las tarjetas de una lista, que tienen nombres distintos, dales además una clase de transición:

.tarjeta-ruta {
  view-transition-class: tarjeta;
}

::view-transition-group(*.tarjeta) {
  animation-duration: 0.5s;
  animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
}

*.tarjeta significa “cualquier grupo que tenga la clase de transición tarjeta”. Es una propiedad reciente; si el navegador no la entiende, esas tarjetas usan la animación por defecto.

Transiciones dentro de la misma página: startViewTransition()

En una página que se actualiza con JavaScript (filtros, pestañas, cambiar de vista, reordenar), no hay navegación que active la transición. La lanzas tú con document.startViewTransition(), que recibe una función que cambia el DOM:

const lista = document.querySelector('#productos');
const botonVista = document.querySelector('#cambiar-vista');

function cambiarVista() {
  lista.classList.toggle('en-cuadricula');
}

botonVista.addEventListener('click', () => {
  document.startViewTransition(cambiarVista);
});

Paso a paso, al pulsar el botón:

  1. El navegador captura el estado actual.
  2. Ejecuta cambiarVista(), que cambia la clase.
  3. Captura el estado nuevo y anima entre ambos con los pseudoelementos que ya conoces.

Si cada producto tiene su propio view-transition-name, verás cómo cada uno vuela a su nueva posición, sin calcular nada. Es justo lo que hace el ejemplo de “Pruébalo”.

Comprobar el soporte

document.startViewTransition no existe en navegadores antiguos, y llamarla daría un error que rompería la página. Comprueba antes si existe; si no, haz el cambio sin animación:

function conTransicion(actualizar) {
  if (!document.startViewTransition) {
    actualizar();
    return;
  }
  document.startViewTransition(actualizar);
}

botonVista.addEventListener('click', () => conTransicion(cambiarVista));

Así la funcionalidad no depende de la animación: el usuario siempre ve el cambio.

Cambios que esperan datos

La función puede ser async. El navegador espera a que termine antes de capturar el estado nuevo, así que sirve para cambios que cargan datos con fetch:

document.startViewTransition(async () => {
  const respuesta = await fetch('/api/rutas?zona=picos');
  const rutas = await respuesta.json();
  pintarRutas(rutas);
});

Cuidado: mientras se espera, la página se queda congelada con la captura vieja y no responde a clics. Si la petición tarda, el usuario cree que la web se ha colgado. Carga primero los datos y llama a startViewTransition() solo para pintarlos.

El objeto que devuelve

startViewTransition() devuelve un objeto con varias promesas útiles:

const transicion = document.startViewTransition(cambiarVista);

transicion.finished.then(() => {
  console.log('Animación terminada');
});
  • finished: se cumple cuando la animación ha acabado y la capa ha desaparecido.
  • ready: se cumple cuando los pseudoelementos ya existen y la animación va a empezar.
  • skipTransition(): un método que salta la animación y deja directamente el estado nuevo.

Respetar prefers-reduced-motion

Una página entera que se desliza o fotos que vuelan por la pantalla son justo el tipo de movimiento que puede marear a algunas personas. Si el usuario ha activado “reducir movimiento” en su sistema, quita las animaciones de la transición:

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

El !important asegura que gane a cualquier animación personalizada que hayas escrito más arriba. El cambio de estado sigue ocurriendo; solo desaparece el movimiento.

Para las transiciones entre páginas hay otra opción más limpia: activarlas solo para quien no ha pedido nada.

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

Tienes más sobre esta preferencia en CSS accesible.

¿Y las animaciones ligadas al scroll?

Otra forma de animar sin JavaScript es conectar una animación al scroll con animation-timeline: scroll() o view(): barras de progreso de lectura, secciones que aparecen al entrar en pantalla… Ya lo tienes explicado en animaciones. La diferencia con las transiciones de vista es que aquellas avanzan con el desplazamiento, y estas se lanzan una vez cuando cambia el estado de la página.

Cuándo usar transiciones de vista

Úsalas para ayudar a entender qué ha cambiado:

  • Navegar entre páginas de un mismo sitio (fundido suave y cabecera fija).
  • Pasar de un listado al detalle de un elemento (la foto que crece).
  • Cambiar entre vista de lista y de cuadrícula.
  • Reordenar, filtrar o eliminar elementos de una lista.

Evítalas, o hazlas muy cortas, cuando el usuario repite la acción muchas veces seguidas (escribir en un buscador que filtra a cada letra), porque cada transición bloquea los clics mientras dura. Un buen rango es entre 200 y 400 ms.

Errores frecuentes

  • Poner @view-transition solo en una página. Tiene que estar en la de origen y en la de destino. Ponla en la hoja de estilos común.
  • Repetir un view-transition-name. Dos elementos visibles con el mismo nombre cancelan la transición entera. Cada elemento necesita un nombre único.
  • Llamar a startViewTransition() sin comprobar si existe. En navegadores sin soporte la página se rompe. Usa if (!document.startViewTransition).
  • Hacer la espera dentro de la transición. Una petición lenta deja la página congelada. Carga antes los datos.
  • Olvidar prefers-reduced-motion. Una página que se desliza entera puede marear. Quita la animación o activa @view-transition solo con no-preference.
  • Esperar transiciones a otra web. Solo funcionan entre páginas del mismo origen.
  • Animaciones largas. Más de medio segundo resulta lento y bloquea la interacción.

Resumen

HerramientaPara quéEjemplo
@view-transition { navigation: auto; }Transición al navegar entre páginas del mismo sitioEn la hoja de estilos común
view-transition-nameQue un elemento viaje de su posición vieja a la nuevaview-transition-name: foto-cares
view-transition-classEstilar varios grupos a la vez::view-transition-group(*.tarjeta)
::view-transition-group(nombre)Mover y redimensionaranimation-duration: 0.4s
::view-transition-old(nombre)Estado viejo (captura)Animación de salida
::view-transition-new(nombre)Estado nuevo (vivo)Animación de entrada
document.startViewTransition(fn)Transición al cambiar el DOMCambiar de lista a cuadrícula
transicion.finishedSaber cuándo acaba.then(...)
prefers-reduced-motion: reduceQuitar el movimientoanimation: none

En la siguiente lección verás cómo hacer que tus estilos funcionen para todo el mundo: CSS accesible.

Pruébalo tú

Cambia el código y pulsa Ejecutar (o Ctrl + Enter).

view-transitions.html
Resultado

Pon a prueba lo que has aprendido

[CSS] Has puesto esta regla solo en la hoja de estilos de index.html. Al ir a horarios.html, que usa otra hoja sin ella, no hay transición. ¿Por qué?
@view-transition {
  navigation: auto;
}

[CSS] En un listado, todas las tarjetas tienen la misma regla. Al lanzar la transición, no se anima nada. ¿Qué falla?
.tarjeta {
  view-transition-name: tarjeta;
}

[CSS] ¿Qué pseudoelemento representa la captura del estado anterior de la página entera?

[CSS] ¿Qué hace este código en un navegador que no tiene la View Transitions API?
boton.addEventListener('click', () => {
  if (!document.startViewTransition) {
    cambiarVista();
    return;
  }
  document.startViewTransition(cambiarVista);
});

[CSS] ¿Por qué no conviene hacer una petición lenta con fetch dentro de la función que pasas a startViewTransition()?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.