Transiciones de vista del mismo documento para aplicaciones de una sola página

Fecha de publicación: 17 de agosto de 2021; última actualización: 25 de septiembre de 2024

Cuando una transición de vista se ejecuta en un solo documento, se denomina transición de vista del mismo documento. Este suele ser el caso en las aplicaciones de una sola página (SPA), en las que se usa JavaScript para actualizar el DOM. Las transiciones de vista en el mismo documento se admiten en Chrome a partir de la versión 111.

Para activar una transición de vista del mismo documento, llama a document.startViewTransition:

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

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

Cuando se invoca, el navegador captura automáticamente instantáneas de todos los elementos que tienen una propiedad CSS view-transition-name declarada en ellos.

Luego, ejecuta la devolución de llamada que se pasó y que actualiza el DOM, después de lo cual toma instantáneas del nuevo estado.

Luego, estas instantáneas se organizan en un árbol de seudoelementos y se animan con el poder de las animaciones CSS. Los pares de instantáneas del estado anterior y el nuevo realizan una transición fluida desde su posición y tamaño anteriores a su nueva ubicación, mientras que su contenido se desvanece de forma cruzada. Si lo deseas, puedes usar CSS para personalizar las animaciones.


La transición predeterminada: Fundido cruzado

La transición de vista predeterminada es una transición cruzada, 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));
}

Aquí updateTheDOMSomehow cambia el DOM al nuevo estado. Puedes hacerlo como quieras. Por ejemplo, puedes agregar o quitar elementos, cambiar nombres de clases o cambiar estilos.

Y, así de simple, las páginas se difuminan:

Atenuación cruzada predeterminada. Demostración mínima Fuente.

De acuerdo, un fundido cruzado no es tan impresionante. Afortunadamente, las transiciones se pueden personalizar, pero primero debes comprender cómo funcionó este desvanecimiento cruzado básico.


Cómo funcionan estas transiciones

Actualicemos 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 instantánea.

Una vez que se complete, se llamará a la devolución de llamada que se pasó a .startViewTransition(). Ahí es donde se cambia el DOM. Luego, la API captura el nuevo estado de la página.

Una vez que se captura el nuevo estado, la API construye un árbol de seudoelementos de la siguiente manera:

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

El ::view-transition se encuentra en una capa superpuesta, sobre todo lo demás en la página. Esto es útil si deseas establecer 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 se anima de opacity: 1 a opacity: 0, mientras que la vista nueva se anima de opacity: 0 a opacity: 1, lo que crea un encadenado.

Todas las animaciones se realizan con animaciones CSS, por lo que se pueden personalizar con CSS.

Personaliza la transición

Todos los pseudoelementos de la transición de vista se pueden segmentar con CSS y, como las animaciones se definen con CSS, puedes modificarlas con las propiedades de animación de CSS existentes. Por ejemplo:

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

Con ese cambio, la transición ahora es muy lenta:

Fundido cruzado largo. Demostración mínima Fuente.

De acuerdo, eso no es impresionante. En cambio, el siguiente código implementa 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 aquí está el resultado:

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

Transición de varios elementos

En la demostración anterior, toda la página participa en la transición de eje compartido. Eso funciona para la mayor parte de la página, pero no parece ser lo correcto para el encabezado, ya que se desliza hacia afuera solo 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 le 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, que significa que no hay nombre de transición). Se usa para identificar el elemento de forma única durante la transición.

Y el resultado de eso es el siguiente:

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

Ahora el encabezado permanece en su lugar y se desvanece de forma cruzada.

Esa declaración de CSS provocó 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. Uno para el encabezado y otro para el resto. Se pueden segmentar de forma independiente con CSS y se les pueden aplicar diferentes transiciones. Aunque, en este caso, main-header se dejó con la transición predeterminada, que es una transición cruzada.

Bueno, está bien, la transición predeterminada no es solo una transición cruzada, sino que ::view-transition-group también realiza una transición:

  • Posiciona y transforma (con un transform)
  • Ancho
  • Altura

Esto no había importado hasta ahora, ya que el encabezado tiene el mismo tamaño y posición en ambos lados del cambio del DOM. Pero también puedes extraer el texto del encabezado:

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

Se usa fit-content para que el elemento tenga 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, en lugar de mantener el mismo tamaño en ambas páginas.

Ahora tenemos tres partes con las que podemos jugar:

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

Pero, de nuevo, solo usaremos los valores predeterminados:

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

Ahora, el texto del encabezado se desliza de forma satisfactoria para dejar espacio para el botón de atrás.


Anima varios seudoelementos de la misma manera con view-transition-class

Browser Support

  • Chrome: 125.
  • Edge: 125.
  • Firefox Technology Preview: supported.
  • Safari: 18.2.

Source

Supongamos que tienes una transición de vista con varias tarjetas, pero también un título en la página. Para animar todas las tarjetas, excepto el título, debes escribir un selector que apunte a cada tarjeta individual.

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }

#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),

::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

¿Tienes 20 elementos? Eso significa que debes escribir 20 selectores. ¿Quieres agregar un elemento nuevo? Luego, también debes aumentar el selector que aplica los estilos de animación. No es exactamente escalable.

El view-transition-class se puede usar en los seudoelementos de transición de vista para aplicar la misma regla de estilo.

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }

#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

En el siguiente ejemplo de tarjetas, se aprovecha el fragmento de CSS anterior. Todas las tarjetas, incluidas las que se agregaron recientemente, reciben la misma sincronización con un selector: html::view-transition-group(.card).

Grabación de la demostración de tarjetas. Con view-transition-class, se aplica el mismo animation-timing-function a todas las tarjetas, excepto a las que se agregaron o quitaron.

Cómo depurar transiciones

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

Con el panel Animaciones, puedes pausar la siguiente animación y, luego, avanzar y retroceder en ella. Durante este proceso, los seudoelementos de transición se pueden encontrar en el panel Elements.

Depuración de transiciones de vista con las Herramientas para desarrolladores de Chrome.

Los elementos de transición no tienen que ser el mismo elemento del DOM.

Hasta ahora, usamos view-transition-name para crear elementos de transición separados para el encabezado y el texto del encabezado. Conceptualmente, son el mismo elemento antes y después del cambio en el DOM, pero puedes crear transiciones en las que no sea así.

Por ejemplo, se le puede asignar 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 puede 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 sería el siguiente:

Un elemento que realiza la transición a otro. Demostración mínima Fuente.

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

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


Transiciones de entrada y salida personalizadas

Mira este ejemplo:

Entrada y salida de la barra lateral. Demostración mínima Fuente.

La barra lateral es 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 seudoelementos de transición se ven 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, no estará presente el seudoelemento ::view-transition-old(sidebar). Como no hay una imagen "antigua" 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, la barra lateral realiza una transición diferente según si entra, sale o está presente en ambos estados. Entra deslizándose desde la derecha y con una transición de entrada, sale deslizándose hacia la derecha y con una transición de salida, y permanece en su lugar cuando está presente en ambos estados.

Para crear transiciones específicas de entrada y salida, puedes usar la seudoclase :only-child para segmentar los seudoelementos antiguos o nuevos cuando son el único elemento secundario en el 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 la predeterminada es perfecta.

Actualizaciones asíncronas del DOM y espera de contenido

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

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

La transición no comenzará hasta que se cumpla la promesa. Durante este tiempo, la página se inmoviliza, por lo que las demoras aquí deben mantenerse al mínimo. Específicamente, las recuperaciones de red se deben realizar antes de llamar a .startViewTransition(), mientras la página sigue siendo completamente interactiva, en lugar de realizarlas 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 agresivo:

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 el caso en que la miniatura haga la transición a una imagen más grande, haz lo siguiente:

La miniatura que hace la transición a una imagen más grande. Prueba el sitio de demostración.

La transición predeterminada es la de fundido cruzado, lo que significa que la miniatura podría estar en fundido cruzado 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 iniciar la transición. Lo ideal sería que esto se hiciera antes de llamar a .startViewTransition(), de modo que la página siga siendo interactiva y se pueda mostrar un spinner para indicarle al usuario que se está cargando el contenido. Pero, 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 permanece debajo de la imagen completa. Esto significa que, si la vista nueva no se cargó, la miniatura será visible durante toda la transición. Esto significa que la transición puede comenzar de inmediato y la imagen completa se puede cargar en su propio tiempo.

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

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

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

Un elemento que realiza una 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 se anima desde el tamaño anterior al tamaño posterior. Las vistas nuevas y anteriores tienen un ancho del 100% del grupo y una altura automática, lo que significa que mantienen su relación de aspecto independientemente del tamaño del grupo.

Este es un buen valor predeterminado, pero no es lo que se desea 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 la imagen completa se "desrecorta" a medida que pasa de 1:1 a 16:9.

Para obtener información más detallada, consulta Transiciones de vista: cómo controlar los cambios en la relación de aspecto.


Usa consultas de medios para cambiar las transiciones de diferentes estados del dispositivo

Es posible que desees usar diferentes transiciones en dispositivos móviles y computadoras de escritorio, como en este ejemplo, que realiza un deslizamiento completo desde el costado en dispositivos móviles, pero un deslizamiento más sutil en computadoras de escritorio:

Un elemento que realiza la transición a otro. Demostración mínima Fuente.

Esto se puede lograr con 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 desees cambiar los elementos a los que asignas un view-transition-name según las coincidencias de las consultas de medios.


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

Los usuarios pueden indicar que prefieren un movimiento reducido a través de su sistema operativo, y esa preferencia se expone en 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 quiera ningún movimiento. En lugar del fragmento 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 controlar varios estilos de transición de vistas con tipos de transición de vistas

Browser Support

  • Chrome: 125.
  • Edge: 125.
  • Firefox Technology Preview: supported.
  • Safari: 18.

Source

A veces, una transición de una vista en particular a otra debe tener una transición específicamente adaptada. Por ejemplo, cuando vas a la página siguiente o anterior en una secuencia de paginación, es posible que quieras deslizar el contenido en una dirección diferente según si vas a una página superior o inferior de la secuencia.

Grabación de la demostración de paginación. Usa diferentes transiciones según la página a la que vayas.

Para ello, puedes usar tipos de transición de vista, que te permiten asignar uno o más tipos a una transición de vista activa. Por ejemplo, cuando se realiza la transición a una página superior en una secuencia de paginación, se usa el tipo forwards, y cuando se va a una página inferior, se usa el tipo backwards. Estos tipos solo están activos cuando se captura o se realiza una transición, y cada tipo se puede personalizar a través de CSS para usar diferentes animaciones.

Para usar tipos en una transición de vista del mismo documento, pasa types al método startViewTransition. Para permitir esto, document.startViewTransition también acepta un objeto: update es la función de devolución de llamada que actualiza el DOM y types es un array con los tipos.

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});
.

Para responder a estos tipos, usa el selector :active-view-transition-type(). Pasa el type que deseas segmentar al selector. Esto te permite mantener separados los estilos de varias transiciones de vista, sin que las declaraciones de una interfieran con las de la otra.

Dado que los tipos solo se aplican cuando se captura o se realiza la transición, puedes usar el selector para establecer (o anular) un view-transition-name en un elemento solo para la transición de vista con ese tipo.

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

En la siguiente demostración de paginación, el contenido de la página se desliza hacia adelante o hacia atrás según el número de página al que navegas. Los tipos se determinan cuando se hace clic en ellos y, luego, se pasan a document.startViewTransition.

Para segmentar cualquier transición de vista activa, independientemente del tipo, puedes usar el selector de seudoclase :active-view-transition.

html:active-view-transition {
    
}

Cómo controlar varios estilos de transición de vista con un nombre de clase en la raíz de la transición de vista

A veces, una transición de un tipo de vista en particular a otro debe tener una transición específicamente adaptada. O bien, la navegación hacia atrás debería ser diferente de la navegación hacia adelante.

Transiciones diferentes cuando se vuelve "atrás". Demostración mínima Fuente.

Antes de los tipos de transición, la forma de controlar estos casos era establecer temporalmente un nombre de clase en la raíz de la transición. Cuando se llama a document.startViewTransition, esta raíz de transición es el elemento <html>, al que se puede acceder con document.documentElement en JavaScript:

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');
}

Para quitar las clases después de que finalice la transición, este ejemplo usa transition.finished, una promesa que se resuelve una vez que la transición alcanza su estado final. Otras propiedades de este objeto se describen en la referencia de la API.

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 los elementos que obtienen un view-transition-name.


Ejecuta transiciones sin congelar otras animaciones

Mira esta demostración de un video que cambia de posición:

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

¿Notaste algo erróneo? No te preocupes si no lo hiciste. Aquí se muestra en cámara lenta:

Transición de video, más lenta. Demostración mínima Fuente.

Durante la transición, el video parece detenerse y, luego, se muestra 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 en vivo de la vista nueva.

Puedes solucionar este problema, pero primero pregúntate si vale la pena hacerlo. Si no viste el "problema" cuando la transición se reprodujo a su velocidad normal, no me molestaría en cambiarla.

Si realmente quieres corregirlo, no muestres el ::view-transition-old(video); cambia directamente al ::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.

Transición de video, más lenta. Demostración mínima Fuente.

Ahora el video se reproduce durante toda la transición.


Integración con la API de Navigation (y otros frameworks)

Las transiciones de vista se especifican de tal manera que se pueden integrar con otros frameworks o bibliotecas. Por ejemplo, si tu aplicación de una sola página (SPA) usa un router, puedes ajustar el mecanismo de actualización del router para actualizar el contenido con una transición de vista.

En el siguiente fragmento de código, extraído de esta demostración de paginación, se ajusta el controlador de interceptación de la API de Navigation para llamar a document.startViewTransition cuando se admiten las transiciones de vista.

navigation.addEventListener("navigate", (e) => {
    // Don't intercept if not needed
    if (shouldNotIntercept(e)) return;

    // Intercept the navigation
    e.intercept({
        handler: async () => {
            // Fetch the new content
            const newContent = await fetchNewContent(e.destination.url, {
                signal: e.signal,
            });

            // The UA does not support View Transitions, or the UA
            // already provided a Visual Transition by itself (e.g. swipe back).
            // In either case, update the DOM directly
            if (!document.startViewTransition || e.hasUAVisualTransition) {
                setContent(newContent);
                return;
            }

            // Update the content using a View Transition
            const t = document.startViewTransition(() => {
                setContent(newContent);
            });
        }
    });
});

Algunos navegadores, pero no todos, proporcionan su propia transición cuando el usuario realiza un gesto de deslizar para navegar. En ese caso, no debes activar tu propia transición de vista, ya que generaría una experiencia del usuario deficiente o confusa. El usuario vería dos transiciones, una proporcionada por el navegador y la otra por ti, que se ejecutan de forma sucesiva.

Por lo tanto, se recomienda evitar que comience una transición de vista cuando el navegador haya proporcionado su propia transición visual. Para lograrlo, verifica el valor de la propiedad hasUAVisualTransition de la instancia de NavigateEvent. La propiedad se establece en true cuando el navegador proporcionó una transición visual. Esta propiedad hasUIVisualTransition también existe en las instancias de PopStateEvent.

En el fragmento anterior, la verificación que determina si se debe ejecutar la transición de vista tiene en cuenta esta propiedad. Cuando no hay compatibilidad con las transiciones de vista del mismo documento o cuando el navegador ya proporcionó su propia transición, se omite la transición de vista.

if (!document.startViewTransition || e.hasUAVisualTransition) {
  setContent(newContent);
  return;
}

En la siguiente grabación, el usuario desliza el dedo para volver a la página anterior. La captura de la izquierda no incluye una verificación de la marca hasUAVisualTransition. La grabación de la derecha incluye la verificación, por lo que se omite la transición de vista manual porque el navegador proporcionó una transición visual.

Comparación del mismo sitio sin (izquierda) y con (derecha) una verificación de hasUAVisualTransition

Animación con JavaScript

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

Transición circular. 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 finaliza con el círculo que tiene un radio hasta la esquina más lejana. Sin embargo, esperamos 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. Otras propiedades de este objeto se describen en la referencia de la API.


Transiciones como mejora

La API de View Transition está diseñada para "encapsular" un cambio en el DOM y crear una transición para él. Sin embargo, la transición debe tratarse como una mejora, es decir, tu app no debe entrar en un estado de "error" si el cambio del DOM se realiza correctamente, pero la transición falla. Lo ideal es que la transición no falle, pero, si lo hace, no debería interrumpir el resto de la experiencia del usuario.

Para que las transiciones se consideren una mejora, asegúrate de no usar promesas de transición de una manera que haga que tu app arroje un error 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 con este ejemplo es que switchView() rechazará la promesa si la transición no puede alcanzar un estado ready, pero eso no significa que la vista no haya podido cambiar. 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:

Qué 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);
  });

  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 rechaza si falla la transición, sino que se resuelve cuando se completa la actualización del DOM y se rechaza si falla.

Si quieres que switchView se resuelva cuando la vista nueva se haya "estabilizado", es decir, cuando se haya completado o se haya omitido hasta el final cualquier transición animada, reemplaza transition.updateCallbackDone por transition.finished.


No es un polyfill, pero…

Esta no es una función fácil de polyfill. Sin embargo, esta función auxiliar facilita mucho las cosas en los navegadores que no admiten transiciones de vista:

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

Y se puede usar de la siguiente manera:

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

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

En los navegadores que no admiten transiciones de vista, 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 los navegadores que admiten transiciones de vista. Esto es útil si tu sitio tiene una preferencia del usuario para inhabilitar las transiciones.


Trabaja con frameworks

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

  • React: Aquí, la clave es flushSync, que aplica un conjunto de cambios de estado de forma síncrona. Sí, hay una advertencia importante sobre el uso de esa API, pero Dan Abramov me asegura que es apropiada en este caso. Como es habitual con React y el código asíncrono, cuando uses las distintas promesas que devuelve startViewTransition, asegúrate de que tu código se ejecute con el estado correcto.
  • Vue.js: La clave aquí es nextTick, que se cumple una vez que se actualiza el DOM.
  • Svelte: Es muy similar a Vue, pero el método para esperar el siguiente cambio es tick.
  • Lit: La clave aquí es la promesa this.updateComplete dentro de los componentes, que se cumple una vez que se actualiza el DOM.
  • Angular: La clave aquí 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(update)

Inicia un nuevo ViewTransition.

update es una función que se llama una vez que se captura el estado actual del documento.

Luego, cuando se cumple la promesa que devuelve updateCallback, la transición comienza en el siguiente fotograma. Si la promesa que devuelve updateCallback se rechaza, se abandona la transición.

const viewTransition = document.startViewTransition({ update, types })

Inicia un ViewTransition nuevo con los tipos especificados.

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

types establece los tipos activos para la transición cuando se captura o se realiza la transición. Inicialmente, está vacío. Consulta viewTransition.types más abajo para obtener más información.

Miembros de la instancia de ViewTransition:

viewTransition.updateCallbackDone

Es una promesa que se cumple cuando se cumple la promesa que muestra updateCallback o se rechaza cuando se rechaza.

La API de View Transition encapsula un cambio en el DOM y crea una transición. Sin embargo, a veces no te importa el éxito o el fracaso de la animación de transición, solo quieres saber si y cuándo se produce el cambio en el DOM. updateCallbackDone es para ese caso de uso.

viewTransition.ready

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

Se rechaza si no se puede iniciar la transición. Esto puede deberse a una configuración incorrecta, como view-transition-names duplicados, o si updateCallback devuelve una promesa rechazada.

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

viewTransition.finished

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

Solo rechaza si updateCallback devuelve 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, se alcanza el estado final, por lo que finished se cumple.

viewTransition.types

Un objeto similar a Set que contiene los tipos de transición de vista activa. Para manipular las entradas, usa sus métodos de instancia clear(), add() y delete().

Para responder a un tipo específico en CSS, usa el selector de seudoclase :active-view-transition-type(type) en la raíz de la transición.

Los tipos se limpian automáticamente cuando finaliza la transición de vista.

viewTransition.skipTransition()

Omitir la parte de animación de la transición

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


Referencia de estilo y transición predeterminados

::view-transition
Es el seudoelemento raíz que cubre la ventana gráfica y contiene cada ::view-transition-group.
::view-transition-group

Posicionamiento absoluto.

Transiciones width y height entre los estados "antes" y "después".

Realiza la transición transform entre el cuadrilátero del espacio de la ventana gráfica "antes" y "después".

::view-transition-image-pair

Se posiciona de forma absoluta para llenar el grupo.

Tiene isolation: isolate para limitar el efecto de mix-blend-mode en las vistas antiguas y nuevas.

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

Se posiciona de forma absoluta en la esquina superior izquierda del contenedor.

Ocupa el 100% del ancho del grupo, pero tiene una altura automática, por lo que mantendrá su proporción de aspecto en lugar de ocupar todo el grupo.

Tiene mix-blend-mode: plus-lighter para permitir una verdadera transición cruzada.

La vista anterior realiza la transición de opacity: 1 a opacity: 0. La nueva vista realiza una transición de opacity: 0 a opacity: 1.


Comentarios

Siempre agradecemos los comentarios de los desarrolladores. Para ello, informa un problema al Grupo de trabajo de CSS en GitHub con sugerencias y preguntas. Agrega el prefijo [css-view-transitions] a tu problema.

Si encuentras un error, informa un error de Chromium.