Volver a las notas

View Transitions: anima cualquier cambio del DOM con una llamada

26 de octubre de 2026·
cssjavascriptanimationviewtransitions
Demo en vivoAbrir a pantalla completa

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, ready se rechaza y la transición se salta. En Chromium me dio un InvalidStateError. 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 un AbortError. Si pasa antes de que empiece a animar, su promesa ready se rechaza y, sin un manejador, Chromium muestra un error no capturado "Transition was skipped". Si pasa a mitad de la animación, ready ya se había cumplido y finished simplemente 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-transition cubre 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: none en 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, pero ViewTransition.transitionRoot sigue 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 de tile-${i}. Está en Chrome 137, Firefox 144 y Safari 18.4.
  • Transiciones entre documentos. La regla @view-transition para webs de varias páginas aún no es Baseline; web-features sigue sin listar soporte en Firefox.

Fuentes