Transiciones de vista entre documentos para aplicaciones de varias páginas

Cuando se produce una transición de vista entre dos documentos diferentes, se denomina transición de vista entre documentos. Por lo general, este es el caso de las aplicaciones de varias páginas (MPA). Las transiciones de vista entre documentos son compatibles con Chrome a partir de la versión 126.

Navegadores compatibles

  • Chrome: 126.
  • Edge: 126.
  • Firefox: No es compatible.
  • Versión preliminar de tecnología de Safari: Compatible.

Las transiciones de vista entre documentos se basan en los mismos elementos y principios que las transiciones de vista del mismo documento, lo que es muy intencional:

  1. El navegador toma instantáneas de los elementos que tienen un view-transition-name único en la página anterior y en la nueva.
  2. El DOM se actualiza mientras se suprime la renderización.
  3. Por último, las transiciones se basan en animaciones CSS.

La diferencia con las transiciones de vista del mismo documento es que, con las transiciones de vista entre documentos, no es necesario llamar a document.startViewTransition para iniciar una transición de vista. En cambio, el activador de una transición de vista entre documentos es una navegación de origen similar de una página a otra, una acción que suele realizar el usuario de tu sitio web haciendo clic en un vínculo.

En otras palabras, no hay una API a la que llamar para iniciar una transición de vista entre dos documentos. Sin embargo, se deben cumplir dos condiciones:

  • Ambos documentos deben existir en el mismo origen.
  • Ambas páginas deben habilitar la transición de vista.

Ambas condiciones se explican más adelante en este documento.


Las transiciones de vista entre documentos se limitan a las navegaciones del mismo origen.

Las transiciones de vista entre documentos se limitan solo a las navegaciones del mismo origen. Una navegación se considera del mismo origen si el origen de ambas páginas participantes es el mismo.

El origen de una página es una combinación del esquema, el nombre de host y el puerto que se usan, como se detalló en web.dev.

Ejemplo de URL con el esquema, el nombre de host y el puerto destacados. En conjunto, forman el origen.
Una URL de ejemplo con el esquema, el nombre de host y el puerto destacados. En conjunto, forman el origen.

Por ejemplo, puedes tener una transición de vista entre documentos cuando navegas de developer.chrome.com a developer.chrome.com/blog, ya que tienen el mismo origen. No puedes tener esa transición cuando navegas de developer.chrome.com a www.chrome.com, ya que son de origen cruzado y del mismo sitio.


Las transiciones de vista entre documentos son opcionales

Para tener una transición de vista entre documentos entre dos documentos, ambas páginas participantes deben habilitar esta opción. Esto se hace con la regla de anidación @view-transition en CSS.

En la regla de anidación @view-transition, establece el descriptor navigation en auto para habilitar las transiciones de vista para la navegación entre documentos del mismo origen.

@view-transition {
  navigation: auto;
}

Si configuras el descriptor navigation en auto, habilitas las transiciones de vista para los siguientes NavigationType:

  • traverse
  • push o replace, si el usuario no inició la activación a través de los mecanismos de la IU del navegador

Las navegaciones excluidas de auto son, por ejemplo, navegar con la barra de direcciones de URL o hacer clic en un favorito, así como cualquier forma de recarga iniciada por el usuario o la secuencia de comandos.

Si una navegación tarda demasiado (más de cuatro segundos en el caso de Chrome), se omite la transición de vista con un TimeoutError DOMException.

Demostración de transiciones de vista entre documentos

Consulta la siguiente demostración que usa transiciones de vista para crear una demostración de Stack Navigator. No hay llamadas a document.startViewTransition() aquí, las transiciones de vista se activan cuando se navega de una página a otra.

Grabación de la demo de Stack Navigator. Se requiere Chrome 126 o una versión posterior.

Cómo personalizar las transiciones de vista entre documentos

Para personalizar las transiciones de vista entre documentos, puedes usar algunas funciones de la plataforma web.

Estas funciones no forman parte de la especificación de la API de View Transition, pero están diseñadas para usarse en conjunto con ella.

Los eventos pageswap y pagereveal

Navegadores compatibles

  • Chrome: 124.
  • Edge: 124.
  • Firefox: No es compatible.
  • Safari: No se admite.

Origen

Para permitirte personalizar las transiciones de vista entre documentos, la especificación HTML incluye dos eventos nuevos que puedes usar: pageswap y pagereveal.

Estos dos eventos se activan para cada navegación entre documentos del mismo origen, independientemente de si está a punto de ocurrir una transición de vista o no. Si está a punto de ocurrir una transición de vista entre las dos páginas, puedes acceder al objeto ViewTransition con la propiedad viewTransition en estos eventos.

  • El evento pageswap se activa antes de que se renderice el último fotograma de una página. Puedes usar esta opción para realizar algunos cambios de último minuto en la página saliente, justo antes de que se tomen las instantáneas anteriores.
  • El evento pagereveal se activa en una página después de que se inicializa o reactiva, pero antes de la primera oportunidad de renderización. Con ella, puedes personalizar la página nueva antes de que se tomen las instantáneas nuevas.

Por ejemplo, puedes usar estos eventos para establecer o cambiar rápidamente algunos valores de view-transition-name o pasar datos de un documento a otro escribiendo y leyendo datos de sessionStorage para personalizar la transición de vista antes de que se ejecute.

let lastClickX, lastClickY;
document.addEventListener('click', (event) => {
  if (event.target.tagName.toLowerCase() === 'a') return;
  lastClickX = event.clientX;
  lastClickY = event.clientY;
});

// Write position to storage on old page
window.addEventListener('pageswap', (event) => {
  if (event.viewTransition && lastClick) {
    sessionStorage.setItem('lastClickX', lastClickX);
    sessionStorage.setItem('lastClickY', lastClickY);
  }
});

// Read position from storage on new page
window.addEventListener('pagereveal', (event) => {
  if (event.viewTransition) {
    lastClickX = sessionStorage.getItem('lastClickX');
    lastClickY = sessionStorage.getItem('lastClickY');
  }
});

Si lo deseas, puedes omitir la transición en ambos eventos.

window.addEventListener("pagereveal", async (e) => {
  if (e.viewTransition) {
    if (goodReasonToSkipTheViewTransition()) {
      e.viewTransition.skipTransition();
    }
  }
}

El objeto ViewTransition en pageswap y pagereveal son dos objetos diferentes. También manejan las diferentes promesas de manera diferente:

  • pageswap: Una vez que se oculta el documento, se omite el objeto ViewTransition anterior. Cuando eso sucede, viewTransition.ready rechaza y viewTransition.finished resuelve.
  • pagereveal: La promesa updateCallBack ya se resolvió en este punto. Puedes usar las promesas viewTransition.ready y viewTransition.finished.

Navegadores compatibles

  • Chrome: 123.
  • Edge: 123.
  • Firefox: No es compatible.
  • Safari: No se admite.

Origen

En los eventos pageswap y pagereveal, también puedes realizar acciones en función de las URLs de las páginas antiguas y nuevas.

Por ejemplo, en el navegador de pila de MPA, el tipo de animación que se debe usar depende de la ruta de navegación:

  • Cuando se navega de la página de descripción general a una página de detalles, el contenido nuevo debe deslizarse de la derecha a la izquierda.
  • Cuando se navega de la página de detalles a la página de descripción general, el contenido anterior debe deslizarse de izquierda a derecha.

Para ello, necesitas información sobre la navegación que, en el caso de pageswap, está a punto de ocurrir o, en el caso de pagereveal, acaba de ocurrir.

Para ello, los navegadores ahora pueden exponer objetos NavigationActivation que contienen información sobre la navegación de origen. Este objeto expone el tipo de navegación utilizado, el actual y las entradas del historial de destino final, como se encuentra en navigation.entries() de la API de Navigation.

En una página activada, puedes acceder a este objeto a través de navigation.activation. En el evento pageswap, puedes acceder a esto a través de e.activation.

Consulta esta demostración de Profiles que usa información de NavigationActivation en los eventos pageswap y pagereveal para establecer los valores de view-transition-name en los elementos que deben participar en la transición de vista.

De esta manera, no tienes que decorar cada elemento de la lista con un view-transition-name por adelantado. En cambio, esto sucede justo a tiempo con JavaScript, solo en los elementos que lo necesitan.

Grabación de la demostración de Perfiles. Requiere Chrome 126 o versiones posteriores.

El código es el siguiente:

// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
  if (e.viewTransition) {
    const targetUrl = new URL(e.activation.entry.url);

    // Navigating to a profile page
    if (isProfilePage(targetUrl)) {
      const profile = extractProfileNameFromUrl(targetUrl);

      // Set view-transition-name values on the clicked row
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';

      // Remove view-transition-names after snapshots have been taken
      // (this to deal with BFCache)
      await e.viewTransition.finished;
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
    }
  }
});

// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
    const fromURL = new URL(navigation.activation.from.url);
    const currentURL = new URL(navigation.activation.entry.url);

    // Navigating from a profile page back to the homepage
    if (isProfilePage(fromURL) && isHomePage(currentURL)) {
      const profile = extractProfileNameFromUrl(currentURL);

      // Set view-transition-name values on the elements in the list
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';

      // Remove names after snapshots have been taken
      // so that we're ready for the next navigation
      await e.viewTransition.ready;
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
    }
  }
});

El código también se limpia después de sí mismo quitando los valores de view-transition-name después de que se ejecutó la transición de vista. De esta manera, la página está lista para navegaciones sucesivas y también puede controlar el recorrido del historial.

Para ayudar con esto, usa esta función de utilidad que establece view-transition-name de forma temporal.

const setTemporaryViewTransitionNames = async (entries, vtPromise) => {
  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = name;
  }

  await vtPromise;

  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = '';
  }
}

El código anterior ahora se puede simplificar de la siguiente manera:

// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
  if (e.viewTransition) {
    const targetUrl = new URL(e.activation.entry.url);

    // Navigating to a profile page
    if (isProfilePage(targetUrl)) {
      const profile = extractProfileNameFromUrl(targetUrl);

      // Set view-transition-name values on the clicked row
      // Clean up after the page got replaced
      setTemporaryViewTransitionNames([
        [document.querySelector(`#${profile} span`), 'name'],
        [document.querySelector(`#${profile} img`), 'avatar'],
      ], e.viewTransition.finished);
    }
  }
});

// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
    const fromURL = new URL(navigation.activation.from.url);
    const currentURL = new URL(navigation.activation.entry.url);

    // Navigating from a profile page back to the homepage
    if (isProfilePage(fromURL) && isHomePage(currentURL)) {
      const profile = extractProfileNameFromUrl(currentURL);

      // Set view-transition-name values on the elements in the list
      // Clean up after the snapshots have been taken
      setTemporaryViewTransitionNames([
        [document.querySelector(`#${profile} span`), 'name'],
        [document.querySelector(`#${profile} img`), 'avatar'],
      ], e.viewTransition.ready);
    }
  }
});

Espera a que se cargue el contenido con el bloqueo de renderización

Navegadores compatibles

  • Chrome: 124.
  • Edge: 124.
  • Firefox: No es compatible.
  • Safari: No se admite.

En algunos casos, es posible que desees retrasar la primera renderización de una página hasta que un elemento determinado esté presente en el nuevo DOM. Esto evita que se produzcan parpadeos y garantiza que el estado al que animas sea estable.

En <head>, define uno o más IDs de elementos que deben estar presentes antes de que la página se renderice por primera vez con la siguiente metaetiqueta.

<link rel="expect" blocking="render" href="#section1">

Esta metaetiqueta significa que el elemento debe estar presente en el DOM, no que se debe cargar el contenido. Por ejemplo, con las imágenes, la sola presencia de la etiqueta <img> con el id especificado en el árbol del DOM es suficiente para que la condición se evalúe como verdadera. Es posible que la imagen aún se esté cargando.

Antes de usar el bloqueo de renderización por completo, ten en cuenta que la renderización incremental es un aspecto fundamental de la Web, por lo que debes tener cuidado cuando optes por bloquear la renderización. El impacto de bloquear la renderización debe evaluarse caso por caso. De forma predeterminada, evita usar blocking=render, a menos que puedas medir y evaluar de forma activa el impacto que tiene en tus usuarios, midiendo el impacto en tus Métricas web esenciales.


Cómo ver los tipos de transición en las transiciones de vista entre documentos

Las transiciones de vista entre documentos también admiten tipos de transición de vista para personalizar las animaciones y los elementos que se capturan.

Por ejemplo, cuando vayas a la página siguiente o a la anterior en una paginación, es posible que desees usar animaciones diferentes según si vas a una página más alta o más baja de la secuencia.

Para configurar estos tipos de antemano, agrégalos en la regla de @view-transition:

@view-transition {
  navigation: auto;
  types: slide, forwards;
}

Para establecer los tipos sobre la marcha, usa los eventos pageswap y pagereveal para manipular el valor de e.viewTransition.types.

window.addEventListener("pagereveal", async (e) => {
  if (e.viewTransition) {
    const transitionType = determineTransitionType(navigation.activation.from, navigation.activation.entry);
    e.viewTransition.types.add(transitionType);
  }
});

Los tipos no se transfieren automáticamente del objeto ViewTransition de la página anterior al objeto ViewTransition de la página nueva. Debes determinar los tipos que se usarán, al menos, en la página nueva para que las animaciones se ejecuten como se espera.

Para responder a estos tipos, usa el selector de pseudoclase :active-view-transition-type() de la misma manera que con las transiciones de vista del mismo documento.

/* 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 */
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;
  }
}

Debido a que los tipos solo se aplican a una transición de vista activa, se limpian automáticamente cuando finaliza una transición de vista. Por eso, los tipos funcionan bien con funciones como BFCache.

Demostración

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 navegas.

Grabación de la demostración de paginación (MPA). Usa diferentes transiciones según la página a la que te diriges.

El tipo de transición que se debe usar se determina en los eventos pagereveal y pageswap a partir de las URLs de origen y destino.

const determineTransitionType = (fromNavigationEntry, toNavigationEntry) => {
  const currentURL = new URL(fromNavigationEntry.url);
  const destinationURL = new URL(toNavigationEntry.url);

  const currentPathname = currentURL.pathname;
  const destinationPathname = destinationURL.pathname;

  if (currentPathname === destinationPathname) {
    return "reload";
  } else {
    const currentPageIndex = extractPageIndexFromPath(currentPathname);
    const destinationPageIndex = extractPageIndexFromPath(destinationPathname);

    if (currentPageIndex > destinationPageIndex) {
      return 'backwards';
    }
    if (currentPageIndex < destinationPageIndex) {
      return 'forwards';
    }

    return 'unknown';
  }
};

Comentarios

Siempre agradecemos los comentarios de los desarrolladores. Para compartirlas, informa un problema al grupo de trabajo del 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.