Transiciones de vista en CSS: View Transitions API
En esta lección
- La idea: dos fotos y una animación
- Transiciones entre páginas con @view-transition
- Dar nombre a los elementos: view-transition-name
- Los pseudoelementos de la transición
- Personalizar la animación
- Transiciones dentro de la misma página: startViewTransition()
- Respetar prefers-reduced-motion
- ¿Y las animaciones ligadas al scroll?
- Cuándo usar transiciones de vista
- Errores frecuentes
- 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:
- El navegador hace una captura del estado actual (el estado “viejo”).
- Tú cambias la página: navegas a otra, cambias clases, añades o quitas elementos.
- El navegador toma el estado nuevo, que sigue vivo (no es una foto).
- 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:
| Tipo | Cuándo | Cómo se activa |
|---|---|---|
| Entre documentos (cross-document) | Al navegar de una página a otra del mismo sitio | Solo CSS: @view-transition |
| En el mismo documento (same-document) | Al cambiar el DOM sin recargar | JavaScript: 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
rootestá 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)
| Pseudoelemento | Qué representa | Animación por defecto |
|---|---|---|
::view-transition | La capa que cubre toda la ventana | Ninguna |
::view-transition-group(nombre) | La caja de un elemento con nombre | Mueve y redimensiona de la posición vieja a la nueva |
::view-transition-image-pair(nombre) | Agrupa las dos imágenes | Ninguna |
::view-transition-old(nombre) | La captura del estado viejo | Se desvanece (de opacidad 1 a 0) |
::view-transition-new(nombre) | El estado nuevo, vivo | Aparece (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:
- El navegador captura el estado actual.
- Ejecuta
cambiarVista(), que cambia la clase. - 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-transitionsolo 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. Usaif (!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-transitionsolo conno-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
| Herramienta | Para qué | Ejemplo |
|---|---|---|
@view-transition { navigation: auto; } | Transición al navegar entre páginas del mismo sitio | En la hoja de estilos común |
view-transition-name | Que un elemento viaje de su posición vieja a la nueva | view-transition-name: foto-cares |
view-transition-class | Estilar varios grupos a la vez | ::view-transition-group(*.tarjeta) |
::view-transition-group(nombre) | Mover y redimensionar | animation-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 DOM | Cambiar de lista a cuadrícula |
transicion.finished | Saber cuándo acaba | .then(...) |
prefers-reduced-motion: reduce | Quitar el movimiento | animation: 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).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.