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

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

Navegadores compatibles

  • Chrome: 126
  • Borde: 126
  • Firefox: No es compatible.
  • Safari: no es compatible.

Las transiciones de vistas entre documentos se basan en los mismos principios y componentes básicos que las transiciones de vista del mismo documento, lo cual 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. Y, por último, las transiciones funcionan con animaciones de CSS.

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

En otras palabras, no hay una API a la cual 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 habilitarse para permitir la transición de vistas.

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 a las navegaciones del mismo origen únicamente. Se considera que una navegación es de un mismo origen si el origen de las dos 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 usaron, como se detalla en web.dev.

Una URL de ejemplo con el esquema, el nombre de host y el puerto destacados. Combinadas, forman el origen.
Una URL de ejemplo con el esquema, el nombre de host y el puerto destacados. Combinadas, 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 esas son del mismo origen. No es posible realizar esa transición cuando navegas de developer.chrome.com a www.chrome.com, ya que son de origen cruzado y del mismo sitio.


Se habilitan las transiciones de vista entre documentos

Para que se realice una transición de vista de varios documentos entre dos documentos, ambas páginas participantes deben habilitar esta opción. Para ello, se usa la regla-at @view-transition en CSS.

En la regla at @view-transition, establece el descriptor navigation en auto para habilitar las transiciones de vistas en las navegaciones del mismo origen entre documentos.

@view-transition {
  navigation: auto;
}

Si configuras el descriptor navigation como auto, aceptas que se produzcan transiciones de vistas en los siguientes objetos NavigationType:

  • traverse
  • push o replace, si el usuario no inició la activación mediante 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 la navegación tarda demasiado (más de cuatro segundos en el caso de Chrome), se omitirá la transición de vista con un DOMException TimeoutError.

Demostración de transiciones de vistas entre documentos

Mira la siguiente demostración en la que se usan transiciones de vista para crear una demostración de Stack Navigator. En este caso, no hay llamadas a document.startViewTransition(); las transiciones de vistas se activan cuando se navega de una página a otra.

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

Cómo personalizar transiciones de vista entre documentos

Existen algunas funciones de la plataforma web que puedes usar para personalizar las transiciones de vistas entre documentos.

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

Los eventos pageswap y pagereveal

Navegadores compatibles

  • Chrome: 124.
  • Edge: 124
  • Firefox: No es compatible.
  • Safari: no es compatible.

Origen

Para poder personalizar las transiciones de vistas 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 una transición de vista está por suceder 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 marco de una página. Puedes usar esta opción para hacer algunos cambios de último minuto en la página de salida, justo antes de que se tomen las instantáneas anteriores.
  • El evento pagereveal se activa en una página después de que se inicializó o reactivó, pero antes de la primera oportunidad de renderización. Con la herramienta, puedes personalizar la página nueva antes de que se tomen las nuevas instantáneas.

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 vistas antes de que se ejecute realmente.

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 decidir 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 distintas promesas de manera diferente:

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

Navegadores compatibles

  • Chrome: 123
  • Edge: 123
  • Firefox: No es compatible.
  • Safari: no es compatible.

Origen

En los eventos pageswap y pagereveal, también puedes tomar medidas en función de las URLs de las páginas anteriores y nuevas.

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

  • Cuando navegues desde la página de resumen hasta la página de detalles, el contenido nuevo debe deslizarse de derecha a izquierda.
  • Cuando navegues de la página de detalles a la página de resumen, 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, que acaba de ocurrir.

Para ello, los navegadores ahora pueden exponer objetos NavigationActivation que contienen información sobre la navegación del mismo origen. Este objeto expone el tipo de navegación utilizado, las entradas del historial de destino actual y finales, 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 través de e.activation.

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

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

Grabación de la demostración de perfiles. Se requiere Chrome 126 o una versión posterior.

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 limpia después de sí mismo, ya que quita los valores view-transition-name después de que se ejecutó la transición de vista. De esta manera, la página estará lista para navegaciones sucesivas y también puede controlar el recorrido del historial.

Para facilitar esto, usa esta función de utilidad que establece de forma temporal los objetos view-transition-name.

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 es compatible.

En algunos casos, es posible que desees retener la primera representación de una página hasta que un elemento determinado esté presente en el nuevo DOM. De esta manera, se evita la escritura en la memoria flash y se garantiza que el estado al que se está animando sea estable.

En <head>, usa la siguiente metaetiqueta para definir uno o más IDs de elementos que deben estar presentes antes de que la página obtenga su primera renderización.

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

Esta metaetiqueta indica que el elemento debe estar presente en el DOM, no que debe cargarse el contenido. Por ejemplo, en el caso de las imágenes, la mera 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 ahondar en el bloqueo de la representación, ten en cuenta que el procesamiento incremental es un aspecto fundamental de la Web, por lo que debes tener cuidado cuando decidas bloquearlo. El impacto de la renderización de bloqueo se debe evaluar caso por caso. De forma predeterminada, evita usar blocking=render, a menos que puedas medir y medir activamente el impacto que tiene en tus usuarios. Para ello, mide el impacto en tus Métricas web esenciales.


Cómo consultar tipos de transición en transiciones de vistas entre documentos

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

Por ejemplo, cuando vas a la página siguiente o a la anterior en una paginación, es posible que quieras usar diferentes animaciones en función de si vas a una página superior o una inferior de la secuencia.

Para configurar estos tipos por adelantado, agrégalos en la regla at @view-transition:

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

Para configurar 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 en, al menos, la página nueva para que las animaciones se ejecuten como se espera.

Para responder a estos tipos, usa el selector de seudoclase :active-view-transition-type() de la misma manera que con las transiciones de vistas 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;
  }
}

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

Demostración

En la siguiente demostración de paginación, el contenido de la página se desliza hacia delante o hacia atrás según el número de página a la que navegas.

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

El tipo de transición que se usará se determina en los eventos pagereveal y pageswap observando las URLs de entrada y salida.

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

Apreciamos los comentarios de los desarrolladores. Para compartir esta información, 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 te encuentras con un error, informa un error en Chromium en su lugar.