El ciclo completo de una transición
Los nueve pasos entre la llamada y el final, dónde se bloquea el renderizado, y qué puede abortar la transición en cada punto.
La API tiene una superficie mínima —un método y cuatro miembros del objeto que devuelve— y un ciclo de vida sorprendentemente rico. Conocerlo de memoria es lo que separa “funciona casi siempre” de “sé por qué esta vez no ha funcionado”, porque los fallos de las transiciones de vista son casi todos fallos de secuencia: algo que se hizo en el paso equivocado.
- Enumerar los pasos del ciclo en orden y qué ocurre en cada uno.
- Identificar el tramo en el que el renderizado está bloqueado.
- Colocar cada tipo de trabajo en el paso que le corresponde.
- Anticipar en qué puntos la transición puede abortarse.
Los nueve pasos
flowchart TB A[Llamada a startViewTransition con el callback] --> B[El navegador captura el estado antiguo] B --> C[Se bloquea el renderizado del documento] C --> D[Se ejecuta el callback y muta el DOM] D --> E[Se resuelve updateCallbackDone] E --> F[El navegador captura el estado nuevo] F --> G[Se construye el arbol de pseudoelementos] G --> H[Se resuelve ready y arrancan las animaciones] H --> I[Acaban las animaciones y se desmonta el arbol] I --> J[Se resuelve finished] style A fill:#89b4fa,color:#11111b style B fill:#94e2d5,color:#11111b style C fill:#f9e2af,color:#11111b style D fill:#cba6f7,color:#11111b style F fill:#94e2d5,color:#11111b style G fill:#cba6f7,color:#11111b style H fill:#a6e3a1,color:#11111b style J fill:#a6e3a1,color:#11111b
Paso 1, la llamada. document.startViewTransition(callback) devuelve inmediatamente un objeto ViewTransition. La función que le pasas todavía no se ha ejecutado. Esto sorprende la primera vez: el código que sigue a la llamada corre antes que la mutación.
Paso 2, la captura del estado antiguo. El navegador toma una imagen del documento tal y como está pintado en ese instante, y una imagen separada de cada elemento que tenga view-transition-name. Junto con las imágenes guarda su geometría: posición, tamaño y transformación en el espacio de la pantalla.
Paso 3, el bloqueo del renderizado. A partir de aquí y hasta el paso 6, el navegador deja de pintar cambios en pantalla. Lo que se ve es la captura del paso 2, congelada. Es el mecanismo que hace posible que el callback pueda hacer una mutación de varios pasos sin que el usuario vea ningún estado intermedio.
Paso 4, el callback. Se ejecuta tu función. Aquí es donde el DOM cambia. Si la función devuelve una promesa, el navegador espera a que se resuelva antes de continuar; mientras espera, el renderizado sigue bloqueado.
Paso 5, updateCallbackDone. La promesa que informa de que la mutación ha terminado. Se resuelve tanto si hubo transición visual como si no.
Paso 6, la captura del estado nuevo. El navegador recalcula estilo y layout con el DOM ya mutado, y toma las capturas del estado nuevo, con la misma lógica de nombres que en el paso 2.
Paso 7, el árbol de pseudo-elementos. Con las dos series de capturas, el navegador construye un árbol de pseudo-elementos colgado del elemento raíz y lo pinta en la capa superior. Ese árbol es lo que se anima.
Paso 8, ready y las animaciones. Se resuelve la promesa ready, se desbloquea el renderizado y arrancan las animaciones: las de la hoja de estilos del navegador, o las tuyas si las has sobrescrito. Este es el instante exacto en el que puedes intervenir desde JavaScript.
Paso 9, finished. Cuando todas las animaciones del árbol terminan, el navegador desmonta el árbol, devuelve el documento a su renderizado normal y resuelve finished.
Dónde va cada cosa
La utilidad práctica de conocer los pasos es saber colocar el trabajo. Estos son los emparejamientos correctos y los frecuentes errores:
| Lo que quieres hacer | Dónde va | Por qué |
|---|---|---|
| Traer datos de red | Antes de la llamada | Durante el callback la pantalla está congelada |
| Mutar el DOM | En el callback | Es literalmente para lo que existe |
Poner view-transition-name al nuevo elemento |
En el callback | Tiene que existir antes de la captura del paso 6 |
Poner view-transition-name al elemento antiguo |
Antes de la llamada | La captura del paso 2 ya ha ocurrido si esperas |
| Animar con Web Animations sobre los pseudos | Tras await ready |
El árbol no existe antes |
| Limpiar clases temporales | Tras await finished |
Antes desaparecerían a mitad de animación |
| Enfocar el elemento nuevo | Tras await updateCallbackDone |
El DOM ya está, no hace falta esperar más |
Ese cuarto caso es la fuente de una clase de bugs concreta y desconcertante. Si asignas el nombre al elemento de origen dentro del callback, llegas tarde: la captura del estado antiguo ya se hizo sin ese nombre, así que el elemento no tiene pareja y en lugar de transformarse aparece de la nada. El nombre del origen se pone antes de llamar; el del destino, dentro del callback.
async function abrirDetalle(miniatura, url) {
const html = await fetch(url).then((r) => r.text()); // antes: red
miniatura.style.viewTransitionName = 'foto'; // antes: nombre del origen
const t = document.startViewTransition(() => {
document.querySelector('main').innerHTML = html;
document.querySelector('.detalle img').style.viewTransitionName = 'foto';
});
await t.finished;
miniatura.style.viewTransitionName = ''; // despues: limpiar
}
Ese finally implícito al final no es cosmético. Si dejas el nombre puesto en la miniatura y el usuario vuelve atrás, tendrás dos elementos con view-transition-name: foto al mismo tiempo, y eso aborta la transición entera. Es el error más común de toda la API y tiene una lección completa dedicada en el nivel siguiente.
Qué puede abortar la transición
La transición no siempre llega al paso 9, y conviene saber por qué caminos se sale.
Una segunda llamada a startViewTransition. Si una transición está en marcha y empiezas otra, la primera se salta: sus animaciones se cancelan, su promesa ready se rechaza y el DOM se queda en su estado final. Es el comportamiento correcto para una interfaz donde el usuario pincha rápido, y significa que no debes encadenar transiciones esperando a que la anterior termine: la API ya lo resuelve.
skipTransition(). Salta a la parte visual final inmediatamente. El callback ya se habrá ejecutado o se ejecutará igual: saltarse la transición no cancela la mutación. Es la palanca correcta para prefers-reduced-motion y para el botón de “no quiero esperar”.
Nombres duplicados. Dos elementos renderizados con el mismo view-transition-name en el mismo momento hacen que ready se rechace y la transición se salte.
Un callback que falla o tarda demasiado. Si tu función lanza una excepción o su promesa se rechaza, la transición se aborta. Si tarda más de lo que el navegador está dispuesto a esperar, también.
El documento oculto. Si la pestaña deja de ser visible, no hay transición: el navegador no va a animar algo que nadie mira.
De los nueve pasos, hay uno que puede arruinarte la interfaz de una forma que ninguna animación mal hecha conseguiría: el tramo entre el paso 3 y el paso 6, donde el navegador deja de pintar. Ese bloqueo es lo que hace que la API funcione, y también es una congelación real de la interfaz: durante ese tramo el usuario ve una imagen estática, y aunque los eventos siguen llegando al DOM, nada de lo que ocurra se refleja en pantalla. Si tu callback es asíncrono y espera algo lento, has convertido startViewTransition en un alert() visual. Los tres casos que producen esto en producción son siempre los mismos y ninguno parece peligroso al escribirlo: una petición de red dentro del callback, porque parecía cómodo tenerlo todo junto; un await sobre una promesa que a veces no resuelve, típicamente una fuente que no carga o un decode() de una imagen que falla; y un framework que hace la actualización en varios ticks, de modo que tu callback resuelve antes de que el DOM esté realmente listo y capturas un estado a medias. La disciplina que evita los tres: el callback debe ser síncrono siempre que sea posible, y cuando no lo sea, lo único que puede esperar es la promesa de actualización del framework, nunca red ni recursos. Si necesitas datos, tráelos antes; si necesitas una imagen decodificada, decodifícala antes. Y como red de seguridad, ten presente que skipTransition() se puede llamar desde fuera, así que un temporizador propio que la invoque pasado un umbral razonable es un seguro barato en interfaces críticas.