Cambias el DOM como quieras, envuelves ese cambio en document.startViewTransition() y el navegador anima el paso del estado viejo al nuevo. Sin cálculos FLIP, sin librería de animación y sin mantener dos copias de la interfaz vivas a la vez. Las view transitions dentro del mismo documento son Baseline desde el 14 de octubre de 2025, y las piezas que hacen que escalen (view-transition-class y los tipos de transición) se sumaron en enero de 2026.
La demo de arriba es una rejilla bento en la que no hay nada animado a mano. La rejilla simplemente se reordena, se filtra o pasa a lista, y una tarjeta se convierte en un panel completo. Activa Cámara lenta para verlo con calma, o desmarca View Transitions para ver los mismos cambios del DOM sin la API.
Qué es y por qué importa
Animar un cambio de layout solía significar medir cada elemento antes y después, y luego fingir el movimiento con transforms. La View Transition API le da la vuelta. Cuando llamas a startViewTransition(update), el navegador hace una captura de la página actual, ejecuta tu callback update, captura el estado nuevo y anima entre los dos con un árbol de pseudoelementos que se pinta encima de la página.
Por defecto, toda la página es una única captura llamada root y lo que obtienes es un fundido. La hoja de estilos del navegador que define la especificación le da un cuarto de segundo. Si le das a un elemento su propio view-transition-name, tiene su propio grupo, y el navegador lo anima desde su posición y tamaño anteriores hasta los nuevos. De ahí sale el efecto.
Cómo funciona la demo
Todo pasa por una sola función. Esta es una versión recortada de la de la demo:
function transition(type, update) {
if (!document.startViewTransition) {
update(); // Plan B: el mismo cambio, sin animación
return;
}
const vt = document.startViewTransition({ update, types: [type] });
// Si otra transición sustituye a esta antes de que empiece a animar,
// ready se rechaza con un AbortError. Gestiónalo o la consola se quejará.
vt.ready.catch(() => {});
return vt;
}
// Barajar es solo reordenar nodos del DOM
const shuffled = (arr) => arr.map((el) => [Math.random(), el]).sort((a, b) => a[0] - b[0]).map(([, el]) => el);
transition('shuffle', () => grid.append(...shuffled([...grid.children])));
Cada tarjeta lleva un nombre único y una clase compartida:
tile.style.viewTransitionName = `tile-${i}`;
.tile { view-transition-class: tile; }
/* No captures el resto de la página: la barra se actualiza al instante */
:root { view-transition-name: none; }
/* Una sola regla para las doce tarjetas, gracias a la clase */
::view-transition-group(.tile) {
animation-duration: .55s;
animation-timing-function: cubic-bezier(.2, .9, .25, 1.15);
}
Sin view-transition-class tendrías que escribir una regla para tile-0, otra para tile-1 y así sucesivamente. Una clase no tiene que ser única ni crea un grupo: solo es un gancho de estilo para elementos que ya tienen nombre.
El array types es lo que permite que una misma API haga varias coreografías. En CSS, :active-view-transition-type() aplica al elemento raíz mientras se ejecuta una transición de ese tipo:
:root:active-view-transition-type(filter) {
&::view-transition-new(.tile):only-child { animation: pop-in .45s both; }
&::view-transition-old(.tile):only-child { animation: pop-out .3s both; }
}
/* pop-in y pop-out son @keyframes normales (escala + opacidad) */
El truco de :only-child viene de la especificación. Cuando una tarjeta se filtra solo existe en el estado viejo, así que su grupo tiene imagen antigua y no nueva. Las tarjetas que vuelven solo tienen imagen nueva. Así es como la demo hace aparecer y desaparecer tarjetas mientras el resto se recoloca.
La transformación de tarjeta a panel no necesita nada nuevo. El nombre pasa de un elemento a otro dentro del update:
transition('open', () => {
tile.style.viewTransitionName = 'none';
panel.style.viewTransitionName = name; // mismo nombre, otro elemento
panel.hidden = false;
});
Mismo nombre en el estado viejo y en el nuevo significa mismo grupo, así que el navegador anima la caja de la tarjeta hasta la caja del panel y funde el contenido. Además, a esa pareja le doy una clase hero con ::view-transition-group(.hero) { z-index: 1; }. Sin ella, en mi primera versión el panel que crecía se pintaba por debajo de las tarjetas que van después.
Problemas con los que me topé o que comprobé
- Los nombres tienen que ser únicos entre los elementos que se pintan. Si dos elementos visibles comparten
view-transition-name,readyse rechaza y la transición se salta. En Chromium me dio unInvalidStateError. Los elementos ocultos (display: none) no cuentan, y por eso el filtro funciona. - Interrumpir una transición. Llamar a
startViewTransition()mientras hay otra activa salta la anterior con unAbortError. Si pasa antes de que empiece a animar, su promesareadyse rechaza y, sin un manejador, Chromium muestra un error no capturado "Transition was skipped". Si pasa a mitad de la animación,readyya se había cumplido yfinishedsimplemente se resuelve antes. Pulsa Barajar dos veces seguidas en la demo y el registro muestra los dos casos. - Clics durante la animación. La capa
::view-transitioncubre la página. En Chromium, un clic durante la animación iba a<html>y no al botón de debajo, hasta que añadí::view-transition { pointer-events: none; }. - El estado viejo es una foto. La captura antigua es una imagen estática y la nueva está viva, así que es normal ver texto estirado durante un cambio de tamaño. El truco de MDN de poner
height: 100%en::view-transition-old()y::view-transition-new()suaviza la diferencia de proporciones, y la demo lo usa. - El movimiento reducido es cosa tuya. Con el movimiento reducido emulado en Chromium, las transiciones se siguieron ejecutando. La demo pone
animation: noneen los pseudoelementos de la transición dentro de@media (prefers-reduced-motion: reduce), así que los cambios se aplican al instante.
Soporte en navegadores
Según los datos de compatibilidad de MDN, document.startViewTransition() funciona en Chrome 111, Firefox 144 y Safari 18. El objeto de opciones con types y :active-view-transition-type() llegaron en Chrome 125, Firefox 147 y Safari 18.2. view-transition-class está en Chrome 125, Firefox 144 y Safari 18.2. He ejecutado la demo en Chromium 153 y en WebKitGTK 2.52 sin errores en consola; Firefox no lo he podido ejecutar en mi entorno. Cuando el método no existe, la demo muestra un aviso y aplica los mismos cambios al instante, que es justo como la API está pensada para degradarse.
Qué vigilar
- Transiciones limitadas a un elemento. MDN ya documenta
Element.startViewTransition()para ejecutar transiciones dentro de un subárbol, peroViewTransition.transitionRootsigue marcado como experimental y solo está en Chromium. match-element. Da a cada elemento seleccionado un nombre único automático, así que en transiciones dentro del mismo documento te puedes ahorrar el paso detile-${i}. Está en Chrome 137, Firefox 144 y Safari 18.4.- Transiciones entre documentos. La regla
@view-transitionpara webs de varias páginas aún no es Baseline; web-features sigue sin listar soporte en Firefox.