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. Normalmente, este es 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 del mismo documento son compatibles con 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 declarada una propiedad de CSS view-transition-name
.
Luego, ejecuta la devolución de llamada pasada 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 pseudoelementos y se animan con el poder de las animaciones de CSS. Los pares de instantáneas del estado anterior y el nuevo pasan sin problemas de su posición y tamaño anteriores a su ubicación nueva, mientras que su contenido se desvanece. Si lo deseas, puedes usar CSS para personalizar las animaciones.
La transición predeterminada: encadenado
La transición de vista predeterminada es una compaginación, 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));
}
En el que updateTheDOMSomehow
cambia el DOM al estado nuevo. Puedes hacerlo de la forma que quieras. Por ejemplo, puedes agregar o quitar elementos, cambiar nombres de clases o cambiar estilos.
Y así de simple, las páginas se desvanecen:
De acuerdo, una transición no es tan impresionante. Afortunadamente, las transiciones se pueden personalizar, pero primero debes comprender cómo funcionó esta transición básica.
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 completada, se llama a la devolución de llamada que se pasó a .startViewTransition()
. Ahí es donde se cambia el DOM. Luego, la API captura el estado nuevo de la página.
Una vez que se captura el estado nuevo, 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 ::view-transition
se encuentra en una superposición sobre todo lo demás de 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 nueva se anima de opacity: 0
a opacity: 1
, lo que crea una transición.
Toda la animación se realiza usando animaciones de CSS, por lo que se pueden personalizar con CSS.
Personaliza la transición
Todos los pseudoelementos de transición de vistas pueden orientarse con CSS, y dado que 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 único cambio, la atenuación ahora es muy lenta:
Bien, eso no es impresionante. En su lugar, 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 este es el resultado:
Realiza 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 para la mayor parte de la página, pero no parece ser del todo correcto para el encabezado, ya que se desliza hacia afuera solo para volver a deslizarlo 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 un nombre de transición). Se usa para identificar de forma única el elemento durante la transición.
Y el resultado es el siguiente:
Ahora, el encabezado permanece en su lugar y se desvanece.
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. Se pueden segmentar de forma independiente con CSS y se les pueden asignar diferentes transiciones. Sin embargo, en este caso, main-header
se dejó con la transición predeterminada, que es un encadenado.
Bien, la transición predeterminada no es solo una atenuación cruzada, el ::view-transition-group
también realiza transiciones:
- Posición y transformación (con
transform
) - Ancho
- Altura
Eso no ha importado hasta ahora, ya que el encabezado tiene el mismo tamaño y posición en ambos lados del cambio del DOM. Sin embargo, 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 reducir el 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 usaremos los valores predeterminados:
Ahora, el texto del encabezado se desliza de forma satisfactoria para dejar espacio para el botón Atrás.
Anima varios seudoelementos de la misma manera con view-transition-class
Navegadores compatibles
Supongamos que tienes una transición de vista con muchas 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 se oriente a cada una de las tarjetas individuales.
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? Son 20 selectores que debes escribir. ¿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 pseudoelementos de transición de vistas 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)
.
Cómo depurar transiciones
Dado que las transiciones de vistas se crean sobre las animaciones de CSS, el panel Animations en las Herramientas para desarrolladores de Chrome es excelente para depurar transiciones.
Con el panel Animations, puedes pausar la siguiente animación y, luego, arrastrarla hacia adelante y hacia atrás en la animación. Durante este proceso, los pseudoelementos de transición se pueden encontrar en el panel Elementos.
No es necesario que los elementos en transición sean el mismo elemento del DOM
Hasta ahora, usamos view-transition-name
para crear elementos de transición separados para el encabezado y el texto en el encabezado. Conceptualmente, son el mismo elemento antes y después del cambio del DOM, pero puedes crear transiciones en las que no sea así.
Por ejemplo, se puede asignar un view-transition-name
a la incorporación de video principal:
.full-embed {
view-transition-name: full-embed;
}
Luego, cuando se hace 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:
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 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 la fuente para ver la implementación completa.
Transiciones de entrada y salida personalizadas
Observa este ejemplo:
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 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, el pseudoelemento ::view-transition-old(sidebar)
no estará allí. Como no hay una tienda "vieja" 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 transiciones de manera diferente según si está entrando, saliendo o presente en ambos estados. Entra deslizándose desde la derecha y atenuándose, para salir deslizándose hacia la derecha y atenuándose, y permanece en su lugar cuando está presente en ambos estados.
Si quieres crear transiciones específicas de entrada y salida, puedes usar la seudoclase :only-child
para segmentar los seudoelementos antiguos o nuevos cuando sean 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 la configuración predeterminada es perfecta.
Actualizaciones del DOM asíncrono y esperando contenido
La devolución de llamada que se pasa a .startViewTransition()
puede mostrar una promesa, que permite realizar actualizaciones asíncronas del DOM y espera 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 bloquea, por lo que se deben mantener al mínimo los retrasos. Específicamente, las recuperaciones de red deben realizarse antes de llamar a .startViewTransition()
, mientras la página aún es 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 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
Cuando la miniatura pasa a una imagen más grande, sigue estos pasos:
La transición predeterminada es la de fundido cruzado, lo que significa que la miniatura podría tener un 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. Idealmente, esto se haría antes de llamar a .startViewTransition()
, de modo que la página permanezca interactiva y se pueda mostrar un ícono giratorio que le indique al usuario que se están cargando los elementos. 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 se ubica debajo de la imagen completa. Esto significa que, si no se cargó la vista nueva, la miniatura se puede ver durante la transición. Esto significa que la transición puede comenzar de inmediato y que la imagen completa puede cargarse a su propio ritmo.
Esto no funcionaría si la nueva vista incluyera transparencia, pero en este caso sabemos que no lo hace, por lo que podemos hacer 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 tiene una relación de aspecto de 1:1 y la imagen principal tiene una relación de aspecto de 16:9?
En la transición predeterminada, el grupo se anima del tamaño anterior al tamaño posterior. Las vistas anteriores y nuevas 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.
Esta es una buena configuración predeterminada, 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 “se quita” a medida que la transición de 1:1 a 16:9.
Para obtener información más detallada, consulta Transiciones de vista: Cómo controlar los cambios de relación de aspecto.
Usa consultas de medios para cambiar las transiciones de los diferentes estados del dispositivo
Es posible que quieras usar diferentes transiciones en dispositivos móviles y computadoras de escritorio, como en este ejemplo, que muestra una diapositiva completa desde un lado en el dispositivo móvil, pero una diapositiva más sutil en una computadora de escritorio:
Esto se puede lograr con consultas de medios normales:
/* 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;
}
}
Es posible que también quieras cambiar los elementos que asignas a view-transition-name
según las consultas de medios coincidentes.
Reaccionar al "movimiento reducido" preferencia
Los usuarios pueden indicar que prefieren reducir el movimiento a través de su sistema operativo, y esa preferencia se exhibe en CSS.
Puedes optar por evitar cualquier transición para estos usuarios:
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
Sin embargo, se prefiere el "movimiento reducido" no significa que el usuario desea ningún movimiento. En lugar del fragmento anterior, puedes 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 los tipos de transición de vistas
Navegadores compatibles
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 a la anterior en una secuencia de paginación, es posible que quieras deslizar el contenido en una dirección diferente dependiendo de si vas a una página superior o una inferior de la secuencia.
Para ello, puedes usar los 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 realices la transición a una página superior en una secuencia de paginación, usa el tipo forwards
y, cuando vayas a una página inferior, usa el tipo backwards
. Estos tipos solo están activos cuando se captura o se realiza una transición, y cada uno se puede personalizar con CSS para usar diferentes animaciones.
Para usar tipos en una transición de vista del mismo documento, pasa types
al método startViewTransition
. Para permitirlo, 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
al que deseas segmentar en el selector. Esto te permite mantener los estilos de las transiciones de varias vistas separados entre sí, sin que las declaraciones de una interfieran en las declaraciones de la otra.
Debido a 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 demo 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 te diriges. Los tipos se determinan cuando se hace clic en los que se pasan a document.startViewTransition
.
Para orientar cualquier transición de vista activa, independientemente del tipo, puedes usar el selector de seudoclase :active-view-transition
en su lugar.
html:active-view-transition {
…
}
Cómo controlar varios estilos de transición de vista con un nombre de clase en la raíz de transición de vistas
A veces, una transición de un tipo particular de vista a otro debe tener una transición específicamente adaptada. O bien, un 'back' la navegación debe ser diferente de una "adelante" la navegación.
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 una vez finalizada la transición, en este ejemplo, se 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 podría usarse para cambiar qué elementos obtienen un view-transition-name
.
Ejecuta transiciones sin inmovilizar otras animaciones
Mira esta demostración de una posición de transición de video:
¿Viste algo mal? No te preocupes si no lo hiciste. Aquí se ralentiza a la siguiente:
Durante la transición, el video parece congelarse y luego aparece la versión de reproducción del video en forma gradual. 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 solucionarlo. Si no viste el "problema" cuando la transición se reproducía a su velocidad normal, no te molestes en cambiarlo.
Si realmente quieres solucionarlo, no muestres el ::view-transition-old(video)
; cambia directamente al ::view-transition-new(video)
. Para hacerlo, 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.
Ahora, el video se reproduce durante la transición.
Integración en la API de Navigation (y otros frameworks)
Las transiciones de vistas se especifican de 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 tomado de esta demostración de paginación, el controlador de intercepción de la API de Navigation se ajusta 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 el gesto de deslizar el dedo para navegar. En ese caso, no deberías activar tu propia transición de vistas, 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, ejecutándose de forma consecutiva.
Por lo tanto, se recomienda evitar que se inicie 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 NavigateEvent
. La propiedad se establece en true
cuando el navegador proporciona una transición visual. Esta propiedad hasUIVisualTransition
también existe en 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 se admiten las transiciones de vistas del mismo documento o cuando el navegador ya proporcionó su propia transición, se omitirá la transición.
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 comprobación, por lo que se omite la transición de vista manual porque el navegador proporcionó una transición visual.
Animación con JavaScript
Hasta ahora, todas las transiciones se definieron con CSS, pero, a veces, CSS no es suficiente:
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 hasta la esquina más lejana. Sin embargo, esperamos que esto sea posible con CSS en el futuro.
Por suerte, 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 pseudoelementos de transición. Otras propiedades de este objeto se describen en la referencia de la API.
Transiciones como una mejora
La API de View Transition está diseñada para "unir" un cambio de DOM y crear una transición para él. Sin embargo, la transición debe tratarse como una mejora, ya que tu app no debe ingresar un "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 debe interrumpir el resto de la experiencia del usuario.
Para tratar a las transiciones como una mejora, ten cuidado de no usar promesas de transición de una manera que haga que tu app arroje si la transición falla.
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 la vista no pueda 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:
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 la transición falla; se resuelve cuando se completa la actualización del DOM, y se rechaza si falla.
Si deseas que se resuelva switchView
cuando la vista nueva se haya "definido", como en el caso de cualquier transición animada que se haya completado o que se haya omitido hasta el final, reemplaza transition.updateCallbackDone
por transition.finished
.
No es un polyfill, pero...
No es una función fácil de usar en polyfill. Sin embargo, esta función auxiliar facilita mucho las cosas en navegadores que no admiten transiciones de vistas:
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 admitan 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 vista. Esto es útil si tu sitio tiene una preferencia del usuario para inhabilitar las transiciones.
Cómo trabajar 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, te mostramos un conjunto de ejemplos con el colaborador anterior, en varios marcos de trabajo.
- React: 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 aseguró que es apropiado en este caso. Como de costumbre con React y el código asíncrono, cuando uses las diversas promesas que muestrastartViewTransition
, asegúrate de que tu código se ejecute con el estado correcto. - Vue.js: La clave aquí es
nextTick
, que se completa una vez que 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 aquí es la promesa
this.updateComplete
dentro de los componentes, que se entrega una vez que se actualiza el DOM. - Angular: La clave aquí es
applicationRef.tick
, que limpia los cambios pendientes del DOM. A partir de la versión 17 de Angular, puedes usarwithViewTransitions
que viene con@angular/router
.
Referencia de la API
const viewTransition = document.startViewTransition(update)
Inicia una
ViewTransition
nueva.update
es una función a la que se llama una vez que se captura el estado actual del documento.Luego, cuando se cumpla la promesa que muestra
updateCallback
, la transición comenzará en el siguiente fotograma. Si se rechaza la promesa que muestraupdateCallback
, se abandona la transición.const viewTransition = document.startViewTransition({ update, types })
Inicia una
ViewTransition
nueva con los tipos especificadosSe 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 realiza la transición. Inicialmente, está vacío. ConsultaviewTransition.types
más abajo para obtener más información.
Miembros de la instancia de ViewTransition
:
viewTransition.updateCallbackDone
Una promesa que se cumple cuando se cumple la promesa que muestra
updateCallback
o se rechaza cuando se rechaza.La API de View Transition une un cambio de 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 se produce el cambio del DOM y cuándo ocurre.
updateCallbackDone
es para ese caso práctico.viewTransition.ready
Es 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 se puede iniciar la transición. Esto puede deberse a una configuración incorrecta, como
view-transition-name
duplicados, o siupdateCallback
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, se alcanza el estado final, por lo que se completa
finished
.viewTransition.types
Un objeto similar a
Set
que contiene los tipos de la transición de vista activa. Para manipular las entradas, usa sus métodos de instanciaclear()
,add()
ydelete()
.Para responder a un tipo específico en CSS, usa el selector de pseudoclase
:active-view-transition-type(type)
en la raíz de transición.Los tipos se limpian automáticamente cuando finaliza la transición de vista.
viewTransition.skipTransition()
Omite la parte de animación de la transición.
Esto no omitirá llamar a
updateCallback
, ya que el cambio del DOM es independiente de la transición.
Referencia de estilo y transición predeterminados
::view-transition
- El pseudoelemento raíz que cubre la ventana de visualización y contiene cada
::view-transition-group
. ::view-transition-group
Totalmente posicionado.
Transiciones
width
yheight
entre los estados "antes" y "después".Transiciones
transform
entre "antes" y “después” cuadriculado del espacio de la vista del puerto.::view-transition-image-pair
Está perfectamente posicionado para completar el grupo.
Tiene
isolation: isolate
para limitar el efecto demix-blend-mode
en las vistas anteriores y nuevas.::view-transition-new
y::view-transition-old
Se ubica de forma absoluta 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 encadenado.La vista anterior pasa de
opacity: 1
aopacity: 0
. La nueva vista realiza la transición deopacity: 0
aopacity: 1
.
Comentarios
Apreciamos los comentarios de los desarrolladores. Para ello, informa un problema con el 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.