Transições de visualização entre documentos para aplicativos com várias páginas

Quando ocorre uma transição de visualização entre dois documentos diferentes, ela é chamada de transição de visualização de vários documentos. Isso normalmente ocorre em aplicativos de várias páginas (MPA, na sigla em inglês). As transições de visualização entre documentos são compatíveis com o Chrome a partir do Chrome 126.

Compatibilidade com navegadores

  • Chrome: 126
  • Borda: 126.
  • Firefox: incompatível.
  • Safari: incompatível.

As transições de visualização entre documentos dependem dos mesmos elementos básicos e princípios das transições de visualização no mesmo documento, que são muito intencionais:

  1. O navegador cria snapshots dos elementos que têm um view-transition-name exclusivo na página antiga e na nova.
  2. O DOM é atualizado enquanto a renderização é suprimida.
  3. E, finalmente, as transições são alimentadas por animações CSS.
.

A diferença entre as transições de visualização do mesmo documento é que, com as transições de visualização entre documentos, você não precisa chamar document.startViewTransition para iniciar uma transição de visualização. Em vez disso, o gatilho de uma transição de visualização de vários documentos é uma navegação de mesma origem de uma página para outra, uma ação normalmente realizada pelo usuário do site ao clicar em um link.

Em outras palavras, não há uma API a ser chamada para iniciar uma transição de visualização entre dois documentos. No entanto, duas condições precisam ser atendidas:

  • Os dois documentos precisam ter a mesma origem.
  • É necessário ativar as duas páginas para permitir a transição de visualização.

Ambas as condições são explicadas mais adiante neste documento.


As transições de visualização entre documentos são limitadas a navegações de mesma origem

As transições de visualização entre documentos são limitadas apenas a navegações de mesma origem. Uma navegação será considerada de mesma origem se a origem das duas páginas participantes for a mesma.

A origem de uma página é uma combinação do esquema, do nome do host e da porta usados, como detalhado em web.dev.

Um URL de exemplo com o esquema, o nome do host e a porta destacados. Juntas, elas formam a origem.
Um URL de exemplo com o esquema, o nome do host e a porta destacados. Juntas, elas formam a origem.

Por exemplo, você pode fazer uma transição de visualização de vários documentos ao navegar de developer.chrome.com para developer.chrome.com/blog, já que eles têm a mesma origem. Não é possível fazer essa transição ao navegar de developer.chrome.com para www.chrome.com, porque eles são de origem cruzada e do mesmo site.


As transições de visualização entre documentos são opcionais

Para fazer uma transição de visualização de vários documentos entre dois documentos, as duas páginas participantes precisam ativar essa opção. Isso é feito com @view-transition na regra no CSS.

Na regra @view-transition, defina o descritor navigation como auto para ativar transições de visualização em vários documentos com a mesma origem.

@view-transition {
  navigation: auto;
}

Ao definir o descritor navigation como auto, você ativa as transições de visualização para os seguintes NavigationTypes:

  • traverse
  • push ou replace, se a ativação não tiver sido iniciada pelo usuário pelos mecanismos de interface do navegador.

As navegações excluídas do auto incluem, por exemplo, navegar usando a barra de endereço do URL ou clicar em um favorito, além de qualquer forma de recarga iniciada por usuário ou script.

Se uma navegação demorar muito (mais de quatro segundos no caso do Chrome), a transição de visualização será ignorada com um TimeoutError DOMException.

Demonstração das transições de visualização entre documentos

Confira a seguinte demonstração que usa transições de visualização para criar uma demonstração do Stack Navigator. Não há chamadas para document.startViewTransition() aqui. As transições de visualização são acionadas pela navegação de uma página para outra.

Gravação da demonstração do Stack Navigator. Requer o Chrome 126 ou versão mais recente.

Personalizar transições de visualização entre documentos

Para personalizar as transições de visualização entre documentos, existem alguns recursos da plataforma Web que você pode usar.

Esses recursos não fazem parte da especificação da API View Transition, mas foram projetados para serem usados com ela.

Os eventos pageswap e pagereveal

Compatibilidade com navegadores

  • Chrome: 124
  • Borda: 124.
  • Firefox: incompatível.
  • Safari: incompatível.

Origem

Para que você personalize as transições de visualização entre documentos, a especificação HTML inclui dois novos eventos que podem ser usados: pageswap e pagereveal.

Esses dois eventos são disparados para cada navegação de mesma origem em documentos diferentes, independentemente de uma transição de visualização estar prestes a acontecer ou não. Se uma transição de visualização estiver prestes a acontecer entre as duas páginas, acesse o objeto ViewTransition usando a propriedade viewTransition nesses eventos.

  • O evento pageswap é disparado antes que o último frame de uma página seja renderizado. Você pode usar isso para fazer algumas alterações de última hora na página de saída, logo antes dos snapshots antigos serem capturados.
  • O evento pagereveal é disparado em uma página depois que ela é inicializada ou reativada, mas antes da primeira oportunidade de renderização. Com ele, é possível personalizar a nova página antes que os novos snapshots sejam criados.

Por exemplo, é possível usar esses eventos para definir ou mudar rapidamente alguns valores view-transition-name ou transmitir dados de um documento para outro, gravando e lendo dados do sessionStorage para personalizar a transição de visualização antes da execução.

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

Se você quiser, poderá pular a transição nos dois eventos.

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

O objeto ViewTransition em pageswap e pagereveal são dois objetos diferentes. Eles também lidam com as várias promessas de forma diferente:

  • pageswap: depois que o documento é oculto, o antigo objeto ViewTransition é ignorado. Quando isso acontece, viewTransition.ready é rejeitado e viewTransition.finished é resolvido.
  • pagereveal: a promessa de updateCallBack já está resolvida. É possível usar as promessas viewTransition.ready e viewTransition.finished.

Compatibilidade com navegadores

  • Chrome: 123
  • Borda: 123.
  • Firefox: incompatível.
  • Safari: incompatível.

Origem

Nos eventos pageswap e pagereveal, também é possível realizar ações com base nos URLs da página antiga e da nova.

Por exemplo, no MPA Stack Navigator, o tipo de animação a ser usado depende do caminho de navegação:

  • Ao navegar da página de visão geral para uma página de detalhes, o novo conteúdo precisa deslizar da direita para a esquerda.
  • Ao navegar da página de detalhes para a de visão geral, o conteúdo antigo precisa sair da esquerda para a direita.

Para fazer isso, você precisa de informações sobre a navegação que, no caso de pageswap, está prestes a acontecer ou, no caso de pagereveal, acabou de acontecer.

Para isso, os navegadores agora podem expor objetos NavigationActivation que contêm informações sobre a navegação de mesma origem. Esse objeto expõe o tipo de navegação usado, as entradas atuais e finais do histórico de destino, conforme encontradas em navigation.entries() na API Navigation.

Em uma página ativada, é possível acessar esse objeto por meio de navigation.activation. No evento pageswap, é possível acessar pelo e.activation.

Confira esta demonstração de perfis que usa informações de NavigationActivation nos eventos pageswap e pagereveal para definir os valores de view-transition-name nos elementos que precisam participar da transição de visualização.

Dessa forma, não é necessário decorar todos os itens da lista com um view-transition-name no início. Em vez disso, isso acontece no momento certo usando JavaScript, apenas em elementos que precisam dele.

Gravação da demonstração dos perfis. Requer o Chrome 126 ou versão mais recente.

O código é este:

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

O código também é limpo depois de si mesmo, removendo os valores view-transition-name após a execução da transição de visualização. Dessa forma, a página fica pronta para navegações sucessivas e também pode processar a travessia do histórico.

Para ajudar com isso, use essa função utilitária que define view-transition-names temporariamente.

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

O código anterior agora pode ser simplificado da seguinte maneira:

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

Aguardar o conteúdo ser carregado com o bloqueio de renderização

Compatibilidade com navegadores

  • Chrome: 124
  • Borda: 124.
  • Firefox: incompatível.
  • Safari: incompatível.

Em alguns casos, você pode querer interromper a primeira renderização de uma página até que um determinado elemento esteja presente no novo DOM. Isso evita a atualização flash e garante que o estado para o qual você está animando seja estável.

No <head>, defina um ou mais IDs de elementos que precisam estar presentes antes que a página receba a primeira renderização, usando a seguinte metatag.

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

Essa metatag significa que o elemento deve estar presente no DOM e não que o conteúdo deve ser carregado. Por exemplo, no caso de imagens, a simples presença da tag <img> com o id especificado na árvore do DOM é suficiente para que a condição seja avaliada como verdadeira. A imagem ainda pode estar sendo carregada.

Antes de apostar tudo no bloqueio de renderização, saiba que a renderização incremental é um aspecto fundamental da Web, portanto, tenha cuidado ao bloquear a renderização. O impacto do bloqueio da renderização precisa ser avaliado caso a caso. Por padrão, evite usar blocking=render, a menos que você possa medir e avaliar ativamente o impacto disso nos seus usuários, medindo o impacto nas Core Web Vitals.


Conferir os tipos de transição em transições de visualização de vários documentos

As transições de visualização entre documentos também são compatíveis com tipos de transição de visualização para personalizar as animações e quais elementos são capturados.

Por exemplo, ao acessar a próxima página ou a anterior em uma paginação, convém usar animações diferentes, dependendo se você acessa uma página superior ou inferior da sequência.

Para definir esses tipos antecipadamente, adicione os tipos na regra @view-transition:

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

Para definir os tipos rapidamente, use os eventos pageswap e pagereveal para manipular o 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);
  }
});

Os tipos não são transferidos automaticamente do objeto ViewTransition na página antiga para o objeto ViewTransition da nova página. Você precisa determinar os tipos a serem usados pelo menos na nova página para que as animações sejam executadas conforme o esperado.

Para responder a esses tipos, use o seletor de pseudoclasse :active-view-transition-type() da mesma forma que faria com as transições de visualização de mesmo 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;
  }
}

Como os tipos só se aplicam a uma transição de visualização ativa, eles são limpos automaticamente quando uma transição de visualização é concluída. Por isso, os tipos funcionam bem com recursos como BFCache.

Demonstração

Na demonstração de paginação a seguir, o conteúdo da página desliza para frente ou para trás com base no número da página que você está acessando.

Gravação da demonstração de paginação (MPA). Ele usa transições diferentes dependendo da página que você acessar.

O tipo de transição a ser usado é determinado nos eventos pagereveal e pageswap analisando os URLs de e para os URLs.

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

Feedback

O feedback dos desenvolvedores é sempre bem-vindo. Para compartilhar, registre um problema com o grupo de trabalho de CSS no GitHub (em inglês) com sugestões e perguntas. Use [css-view-transitions] como prefixo do problema. Caso tenha um bug, registre um bug do Chromium.