Transiciones fluidas y simples con la API de View Transitions

Navegadores compatibles

  • 111
  • 111
  • x
  • x

Origen

La API de transición de vistas facilita la modificación del DOM en un solo paso y, al mismo tiempo, crea una transición animada entre los dos estados. Está disponible en Chrome 111 y versiones posteriores.

Transiciones creadas con la API de View Transition. Prueba el sitio de demostración: Se requiere Chrome 111 o versiones posteriores.

¿Por qué necesitamos esta función?

Las transiciones de página no solo se ven bien, sino que también comunican la dirección del flujo y dejan claro qué elementos están relacionados entre sí. Incluso pueden ocurrir durante la recuperación de datos, lo que lleva a una percepción más rápida del rendimiento.

Sin embargo, como ya tenemos herramientas de animación en la Web, como las transiciones de CSS, las animaciones de CSS y la API de Web Animation, ¿por qué necesitamos algo nuevo para cambiar de lugar?

La verdad es que las transiciones de estado son difíciles, incluso con las herramientas que ya tenemos.

Incluso algo como un simple fundido cruzado implica que ambos estados estén presentes al mismo tiempo. Esto plantea desafíos de usabilidad, como el manejo de interacciones adicionales con el elemento saliente. Además, para los usuarios de dispositivos de asistencia, hay un período en el que tanto el estado anterior como el posterior están en el DOM al mismo tiempo, y los objetos pueden moverse por el árbol de una manera que es visualmente adecuada, pero puede hacer que se pierdan fácilmente la posición y el enfoque de lectura.

El manejo de los cambios de estado es particularmente complejo si los dos estados difieren en la posición de desplazamiento. Además, si un elemento se mueve de un contenedor a otro, puedes encontrar dificultades con overflow: hidden y otras formas de recorte, lo que significa que debes reestructurar tu CSS para obtener el efecto que deseas.

No es imposible, solo que es muy difícil.

Las transiciones de vistas te ofrecen una forma más sencilla, ya que te permiten realizar el cambio de tu DOM sin ninguna superposición entre los estados, sino también crear una animación de transición entre los estados usando vistas instantáneas.

Además, aunque la implementación actual se orienta a las aplicaciones de una sola página (SPA), esta función se ampliará para permitir transiciones entre las cargas de página completa, lo cual es imposible en este momento.

Estado de estandarización

La función se está desarrollando en el Grupo de trabajo del CSS W3C como borrador de la especificación.

Una vez que estemos satisfechos con el diseño de la API, iniciaremos los procesos y las verificaciones necesarias para lanzar esta función de forma estable.

Los comentarios de los desarrolladores son muy importantes, por lo que te pedimos que informas los problemas en GitHub con sugerencias y preguntas.

La transición más simple: Un encadenado

La transición de vista predeterminada es un encadenado, por lo que sirve como una buena introducción a la API:

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

updateTheDOMSomehow cambia el DOM al estado nuevo. Puedes hacerlo como quieras: agregar o quitar elementos, cambiar los nombres de las clases, cambiar los estilos, etc.

Y así, las páginas se encadenan:

Encadenado predeterminado. Demostración mínima. Fuente.

El encadenado no es tan impresionante. Por suerte, las transiciones se pueden personalizar, pero antes de llegar a eso, debemos entender cómo funcionaba este fundido cruzado básico.

Cómo funcionan estas transiciones

Toma la muestra de código anterior:

document.startViewTransition(() => updateTheDOMSomehow(data));

Cuando se llama a .startViewTransition(), la API captura el estado actual de la página. Esto incluye tomar una captura de pantalla.

Una vez que se completa, se llama a la devolución de llamada pasada a .startViewTransition(). Aquí es donde se cambia el DOM. Luego, la API captura el nuevo estado de la página.

Una vez capturado el estado, la API construye un árbol de pseudoelementos de la siguiente manera:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

El elemento ::view-transition se ubica en una superposición sobre todo lo demás de la página. Resulta útil si deseas definir un color de fondo para la transición.

::view-transition-old(root) es una captura de pantalla de la vista anterior, y ::view-transition-new(root) es una representación en vivo de la vista nueva. Ambos se renderizan como “contenido reemplazado” de CSS (como un <img>).

La vista anterior anima de opacity: 1 a opacity: 0, mientras que la vista nueva anima de opacity: 0 a opacity: 1, lo que crea un encadenado.

Toda la animación se realiza con animaciones de CSS, por lo que se pueden personalizar con CSS.

Personalización sencilla

Todos los pseudoelementos anteriores se pueden orientar con CSS y, dado que las animaciones se definen con CSS, puedes modificarlas usando las propiedades de animación de CSS existentes. Por ejemplo:

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

Con ese único cambio, la atenuación ahora es muy lenta:

Encadenado largo. Demostración mínima. Fuente.

De acuerdo, eso no es impresionante. En su lugar, implementemos la transición de eje compartido de Material Design:

@keyframes fade-in {
  from { opacity: 0; }
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

Y este es el resultado:

Transición de eje compartido. Demostración mínima. Fuente.

Cómo realizar la transición de varios elementos

En la demostración anterior, toda la página está involucrada en la transición de eje compartido. Eso funciona en la mayor parte de la página, pero no parece del todo correcto para el encabezado, ya que se desliza hacia afuera para volver a deslizarse hacia adentro.

Para evitar esto, puedes extraer el encabezado del resto de la página para que se pueda animar por separado. Para ello, se asigna un view-transition-name al elemento.

.main-header {
  view-transition-name: main-header;
}

El valor de view-transition-name puede ser el que desees (excepto none, lo que significa que no hay nombre de transición). Se usa para identificar de forma única el elemento en la transición.

Y el resultado:

Transición de eje compartido con encabezado fijo. Demostración mínima. Fuente.

Ahora el encabezado permanece en su lugar y se atenúa.

Esa declaración de CSS hizo que cambiara el árbol de pseudoelementos:

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

Ahora hay dos grupos de transición. Una para el encabezado y otra para el resto. Estos se pueden segmentar de forma independiente con CSS y tener distintas transiciones. Sin embargo, en este caso, main-header se dejó con la transición predeterminada, que es un encadenado.

La transición predeterminada no es solo un fundido cruzado, sino que ::view-transition-group también realiza las siguientes transiciones:

  • Posicionar y transformar (mediante un transform)
  • Ancho
  • Altura

Eso no es importante hasta ahora, ya que el encabezado tiene el mismo tamaño y posición en ambos lados del DOM. Pero también podemos extraer el texto del encabezado:

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

fit-content se usa para que el elemento sea el tamaño del texto, en lugar de estirarse hasta el ancho restante. Sin esto, la flecha hacia atrás reduce el tamaño del elemento de texto del encabezado, mientras que queremos que sea del mismo tamaño en ambas páginas.

Ahora tenemos tres partes con las que jugar:

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

Pero, de nuevo, solo vamos con los valores predeterminados:

Texto del encabezado deslizante. Demostración mínima. Fuente.

Ahora, el texto del encabezado se desliza un poco satisfactorio para hacer espacio para el botón Atrás.

Cómo depurar transiciones

Dado que las transiciones de vistas se basan en animaciones CSS, el panel Animations de Herramientas para desarrolladores de Chrome es ideal para depurar transiciones.

Con el panel Animations, puedes pausar la siguiente animación y, luego, arrastrarla hacia adelante y atrás por la animación. Durante este proceso, los seudoelementos de transición se pueden encontrar en el panel Elementos.

Cómo depurar transiciones de vistas con las herramientas para desarrolladores de Chrome.

No es necesario que los elementos de transición sean el mismo elemento DOM

Hasta ahora, usamos view-transition-name para crear elementos de transición separados para el encabezado y el texto en este. En términos conceptuales, son el mismo elemento antes y después del cambio del DOM, pero puedes crear transiciones cuando eso no sea el caso.

Por ejemplo, se puede otorgar un view-transition-name al video principal incorporado:

.full-embed {
  view-transition-name: full-embed;
}

Luego, cuando se haga clic en la miniatura, se le podrá asignar el mismo view-transition-name solo durante la transición:

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

Y el resultado:

Un elemento en transición a otro Demostración mínima. Fuente.

La miniatura ahora pasa a la imagen principal. Aunque son elementos conceptualmente (y literalmente) diferentes, la API de transición los trata como lo mismo porque comparten el mismo view-transition-name.

El código real para esto es un poco más complicado que el ejemplo simple anterior, ya que también maneja la transición de vuelta a la página de miniaturas. Consulta la fuente para ver la implementación completa.

Transiciones de entrada y salida personalizadas

Mira este ejemplo:

Acceso a la barra lateral y salida de ella. Demostración mínima. Fuente.

La barra lateral forma parte de la transición:

.sidebar {
  view-transition-name: sidebar;
}

Sin embargo, a diferencia del encabezado del ejemplo anterior, la barra lateral no aparece en todas las páginas. Si ambos estados tienen la barra lateral, los pseudoelementos de transición se verán de la siguiente manera:

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

Sin embargo, si la barra lateral solo está en la página nueva, el seudoelemento ::view-transition-old(sidebar) no estará allí. Como no hay una imagen "anterior" para la barra lateral, el par de imágenes solo tendrá un ::view-transition-new(sidebar). Del mismo modo, si la barra lateral solo está en la página anterior, el par de imágenes solo tendrá un ::view-transition-old(sidebar).

En la demostración anterior, las transiciones de la barra lateral varían según si ingresa, sale o está presente en ambos estados. Entra a la pantalla deslizándose desde la derecha y deslizándose hacia adentro, para salir deslizándose hacia la derecha y desvanecerse, y permanece en su lugar cuando está presente en ambos estados.

Para crear transiciones de entrada y salida específicas, puedes usar la seudoclase :only-child para orientar el seudoelemento antiguo/nuevo cuando sea el único elemento secundario del par de imágenes:

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

En este caso, no hay una transición específica para cuando la barra lateral está presente en ambos estados, ya que el valor predeterminado es perfecto.

Actualizaciones de DOM asíncronos y esperando contenido

La devolución de llamada que se pasa a .startViewTransition() puede mostrar una promesa, lo que permite actualizaciones asíncronas del DOM y espera a que esté listo contenido importante.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

No se iniciará la transición hasta que se cumpla la promesa. Durante este tiempo, la página se bloquea, por lo que las demoras aquí deben reducirse al mínimo. Específicamente, las recuperaciones de red deben realizarse antes de llamar a .startViewTransition(), mientras la página sigue siendo completamente interactiva, en lugar de hacerlo como parte de la devolución de llamada de .startViewTransition().

Si decides esperar a que las imágenes o las fuentes estén listas, asegúrate de usar un tiempo de espera intenso:

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

Sin embargo, en algunos casos, es mejor evitar la demora por completo y usar el contenido que ya tienes.

Aprovecha al máximo el contenido que ya tienes

En caso de que la miniatura pase a una imagen más grande, haz lo siguiente:

La transición predeterminada es el fundido cruzado, lo que significa que la miniatura se puede atenuar de forma cruzada con una imagen completa que aún no se cargó.

Una forma de controlar esto es esperar a que se cargue la imagen completa antes de comenzar la transición. Lo ideal sería que esto se hiciera antes de llamar a .startViewTransition(), de modo que la página permanezca interactiva y se pueda mostrar un ícono giratorio para indicarle al usuario que se están cargando los elementos. Sin embargo, en este caso, hay una mejor manera:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

Ahora la miniatura no se desvanece, sino que queda debajo de la imagen completa. Esto significa que, si no se cargó la vista nueva, la miniatura estará visible durante la transición. Esto significa que la transición puede comenzar de inmediato y la imagen completa se puede cargar a su propio ritmo.

Esto no funcionaría si la nueva vista incluyera transparencia, pero, en este caso, sabemos que no, por lo que podemos hacer esta optimización.

Cómo controlar cambios en la relación de aspecto

Convenientemente, todas las transiciones hasta ahora fueron en elementos con la misma relación de aspecto, pero no siempre será así. ¿Qué sucede si la miniatura es de 1:1 y la imagen principal es de 16:9?

Un elemento en transición a otro, con un cambio en la relación de aspecto. Demostración mínima. Fuente.

En la transición predeterminada, el grupo anima el tamaño anterior al tamaño posterior. La vista anterior y la nueva tienen el 100% del ancho del grupo y la altura automática, lo que significa que mantienen su relación de aspecto independientemente del tamaño del grupo.

Este es un buen parámetro predeterminado, pero no es lo que deseamos en este caso. Entonces:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

Esto significa que la miniatura permanece en el centro del elemento a medida que se expande el ancho, pero se “anula el recorte” de la imagen completa a medida que pasa de 1:1 a 16:9.

Cómo cambiar la transición según el estado del dispositivo

Es posible que desees usar diferentes transiciones en dispositivos móviles y computadoras de escritorio, como en este ejemplo en el que se muestra una diapositiva completa desde el costado en dispositivos móviles, pero una diapositiva más sutil en computadoras:

Un elemento en transición a otro Demostración mínima. Fuente.

Esto puede lograrse usando consultas de medios regulares:

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

También es posible que quieras cambiar los elementos que asignas a una view-transition-name en función de las búsquedas de medios coincidentes.

Cómo reaccionar a la preferencia de "movimiento reducido"

Los usuarios pueden indicar que prefieren el movimiento reducido mediante su sistema operativo, y esa preferencia se expondrá con CSS.

Puedes optar por impedir cualquier transición para estos usuarios:

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

Sin embargo, la preferencia por "movimiento reducido" no significa que el usuario no desee movimiento. En lugar de lo anterior, podrías elegir una animación más sutil, pero que aún exprese la relación entre los elementos y el flujo de datos.

Cómo cambiar la transición según el tipo de navegación

A veces, la navegación de un tipo particular de página a otro debería incluir una transición adaptada específicamente. La navegación hacia atrás debe ser diferente de la navegación hacia adelante.

Transiciones diferentes al volver atrás. Demostración mínima. Fuente.

La mejor manera de manejar estos casos es establecer un nombre de clase en <html>, también conocido como el elemento del documento:

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

En este ejemplo, se usa transition.finished, una promesa que se resuelve una vez que la transición alcanza su estado final. En la referencia de la API, se abordan otras propiedades de este objeto.

Ahora puedes usar ese nombre de clase en tu CSS para cambiar la transición:

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

Al igual que con las consultas de medios, la presencia de estas clases también se podría usar para cambiar qué elementos obtienen un view-transition-name.

Cómo realizar la transición sin congelar otras animaciones

Echa un vistazo a esta demostración de una posición de transición de video:

Transición de video. Demostración mínima. Fuente.

¿Notaste algo malo? No te preocupes si no fue así. Aquí se ralentiza:

La transición de video es más lenta. Demostración mínima. Fuente.

Durante la transición, parece que el video se inmoviliza y luego aparece la versión en reproducción. Esto se debe a que ::view-transition-old(video) es una captura de pantalla de la vista anterior, mientras que ::view-transition-new(video) es una imagen publicada de la vista nueva.

Puedes solucionar este problema, pero primero pregúntate si vale la pena hacerlo. Si no veas el "problema" cuando la transición se esté reproduciendo a su velocidad normal, no me molestaría que lo hicieras.

Si realmente quieres corregirlo, no muestres el ::view-transition-old(video); cambia directamente a ::view-transition-new(video). Para ello, anula los estilos y las animaciones predeterminados:

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

Eso es todo.

La transición de video es más lenta. Demostración mínima. Fuente.

Ahora el video se reproduce durante la transición.

Cómo animar con JavaScript

Hasta ahora, todas las transiciones se definieron con CSS, pero a veces CSS no es suficiente:

Transición de círculo. Demostración mínima. Fuente.

Algunas partes de esta transición no se pueden lograr solo con CSS:

  • La animación comienza en la ubicación del clic.
  • La animación termina con el círculo que tiene un radio hacia la esquina más lejana. No obstante, se espera que esto sea posible con CSS en el futuro.

Afortunadamente, puedes crear transiciones con la API de Web Animation.

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

En este ejemplo, se usa transition.ready, una promesa que se resuelve una vez que se crean correctamente los seudoelementos de transición. En la referencia de la API, se abordan otras propiedades de este objeto.

Transiciones como mejora

La API de transición de vistas está diseñada para "unir" un cambio del DOM y crear una transición para este. Sin embargo, la transición debe tratarse como una mejora, ya que tu app no debería ingresar en un estado de "error" si el cambio del DOM se realiza correctamente, pero la transición falla. Idealmente, la transición no debería fallar, pero si ocurre, no debería interrumpir el resto de la experiencia del usuario.

Para tratar las transiciones como una mejora, ten cuidado de no usar promesas de transición de una manera que podría ocasionar que se arroje tu app si la transición falla.

Qué no debes hacer
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

El problema de este ejemplo es que se rechazará switchView() si la transición no puede alcanzar un estado ready, pero eso no significa que no se haya podido cambiar la vista. Es posible que el DOM se haya actualizado correctamente, pero había view-transition-name duplicados, por lo que se omitió la transición.

En su lugar, siga estos pasos:

async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

En este ejemplo, se usa transition.updateCallbackDone para esperar la actualización del DOM y rechazarla si falla. switchView ya no se rechaza si la transición falla, se resuelve cuando finaliza la actualización del DOM y se rechaza si falla.

Si deseas que switchView se resuelva cuando se haya establecido la nueva vista, como sucede si se completó cualquier transición animada o se omitió hasta el final, reemplaza transition.updateCallbackDone por transition.finished.

No es un polyfill, pero...

No creo que esta función se pueda rellenar de ninguna manera útil, pero me alegra que me equivoque.

Sin embargo, esta función auxiliar facilita mucho las tareas en navegadores que no admiten transiciones de vistas:

function transitionHelper({
  skipTransition = false,
  classNames = [],
  updateDOM,
}) {
  if (skipTransition || !document.startViewTransition) {
    const updateCallbackDone = Promise.resolve(updateDOM()).then(() => {});

    return {
      ready: Promise.reject(Error('View transitions unsupported')),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
    };
  }

  document.documentElement.classList.add(...classNames);

  const transition = document.startViewTransition(updateDOM);

  transition.finished.finally(() =>
    document.documentElement.classList.remove(...classNames)
  );

  return transition;
}

Se puede usar de la siguiente manera:

function spaNavigate(data) {
  const classNames = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    classNames,
    updateDOM() {
      updateTheDOMSomehow(data);
    },
  });

  // …
}

En los navegadores que no son compatibles con las transiciones de vistas, se seguirá llamando a updateDOM, pero no habrá una transición animada.

También puedes proporcionar algunos classNames para agregar a <html> durante la transición, lo que facilita cambiar la transición según el tipo de navegación.

También puedes pasar true a skipTransition si no quieres una animación, incluso en navegadores que admiten transiciones de vistas. Esto resulta útil si tu sitio tiene la preferencia de inhabilitar las transiciones.

Cómo trabajar con frameworks

Si trabajas con una biblioteca o un framework que abstrae los cambios del DOM, la parte complicada es saber cuándo se completa el cambio del DOM. A continuación, se muestra un conjunto de ejemplos con el ayudador anterior, en varios frameworks.

  • Reaccionar: La clave aquí es flushSync, que aplica un conjunto de cambios de estado de forma síncrona. Sí, hay una gran advertencia sobre el uso de esa API, pero Dan Abramov me garantiza que es apropiada en este caso. Como de costumbre con React y el código asíncrono, cuando uses las diversas promesas que muestra startViewTransition, asegúrate de que tu código se ejecute con el estado correcto.
  • Vue.js: La clave es nextTick, que se completa cuando se actualiza el DOM.
  • Svelte: Es muy similar a Vue, pero el método para esperar el próximo cambio es tick.
  • Lit: La clave es la promesa this.updateComplete dentro de los componentes, que se cumple una vez que se actualiza el DOM.
  • Angular: Aquí la clave es applicationRef.tick, que vacía los cambios pendientes del DOM. A partir de la versión 17 de Angular, puedes usar withViewTransitions que se incluye con @angular/router.

Referencia de la API

const viewTransition = document.startViewTransition(updateCallback)

Comienza un nuevo ViewTransition.

Se llama a updateCallback una vez que se captura el estado actual del documento.

Luego, cuando se cumpla la promesa que mostró updateCallback, la transición comenzará en el siguiente fotograma. Si se rechaza la promesa que muestra updateCallback, se abandona la transición.

Miembros de la instancia de ViewTransition:

viewTransition.updateCallbackDone

Una promesa que se cumple cuando se cumple la promesa que muestra updateCallback, o bien se rechaza cuando se rechaza.

La API de transición de vistas une un cambio de DOM y crea una transición. Sin embargo, a veces no importa si la animación de transición es correcta o falla; solo quieres saber si se produce el cambio de DOM y cuándo ocurre. updateCallbackDone es para ese caso de uso.

viewTransition.ready

Una promesa que se cumple una vez que se crean los pseudoelementos para la transición y la animación está a punto de comenzar.

Se rechaza si no puede comenzar la transición. Esto puede deberse a una configuración incorrecta, como elementos view-transition-name duplicados, o a que updateCallback muestra una promesa rechazada.

Esto es útil para animar los pseudoelementos de transición con JavaScript.

viewTransition.finished

Una promesa que se cumple una vez que el estado final es completamente visible e interactivo para el usuario

Solo se rechaza si updateCallback muestra una promesa rechazada, ya que esto indica que no se creó el estado final.

De lo contrario, si una transición no comienza o se omite durante la transición, el estado final aún se alcanza, por lo que se completa finished.

viewTransition.skipTransition()

Omite la parte de animación de la transición.

Esta acción no omitirá la llamada a updateCallback, ya que el cambio del DOM es independiente de la transición.

Referencia predeterminada de estilo y transición

::view-transition
Es el seudoelemento raíz que llena el viewport y contiene cada ::view-transition-group.
::view-transition-group

Totalmente posicionado.

Realiza las transiciones width y height entre los estados “antes” y “después”.

Realiza las transiciones transform entre el cuadrante de espacio de viewport "antes" y "después".

::view-transition-image-pair

En una posición absoluta para ocupar todo el grupo.

Tiene isolation: isolate para limitar el efecto del modo de combinación plus-lighter en la vista nueva y la anterior.

::view-transition-new y ::view-transition-old

Está posicionado absolutamente en la parte superior izquierda del wrapper.

Rellena el 100% del ancho del grupo, pero tiene una altura automática, por lo que mantendrá su relación de aspecto en lugar de llenar el grupo.

Tiene mix-blend-mode: plus-lighter para permitir un verdadero fundido cruzado.

La vista anterior pasa de opacity: 1 a opacity: 0. La vista nueva pasa de opacity: 0 a opacity: 1.

Comentarios

Los comentarios de los desarrolladores son muy importantes en esta etapa. Por lo tanto, informa los problemas en GitHub con sugerencias y preguntas.