Transições no mesmo documento

O problema

Os sites geralmente oferecem várias visualizações de um objeto, por exemplo, uma lista de produtos e uma página de detalhes para cada um deles. A navegação entre as duas visualizações geralmente parece desconectada. Quando um usuário clica na miniatura de um produto para ver os detalhes, ela desaparece, e uma nova imagem maior aparece instantaneamente em outro lugar da tela. Essa falta de continuidade dificulta o acompanhamento das relações entre os elementos.

A solução

A API View Transitions permite especificar pares de elementos que existem em estados diferentes antes e depois de uma transição. Ao acionar uma transição com document.startViewTransition() em uma navegação nas páginas única (SPA), o navegador identifica esses elementos compartilhados pelo view-transition-name exclusivo compartilhado. Em seguida, ele calcula automaticamente a diferença na posição, no tamanho e no estilo e os anima de forma suave do estado antigo para o novo. Essa transição ocorre na camada superior, acima até mesmo de elementos com valores altos de z-index.

Guia de implementação

Etapa 1: agrupar as mudanças de estado em startViewTransition

Para aplicativos de página única (SPAs) ou mudanças de estado simples, envolva a lógica que atualiza o DOM em document.startViewTransition. O navegador captura um snapshot do estado atual, executa a atualização e, em seguida, captura o novo estado.

function navigate(view) {
  // MANDATORY: Wrap the update in startViewTransition
  document.startViewTransition(() => updateDOM(view));
}

Etapa 2: atribuir nomes de transição compartilhados

Use a propriedade CSS view-transition-name para informar ao navegador quais elementos devem ser transformados. O nome pode ser qualquer coisa (exceto none). OBRIGATÓRIO: não pode haver mais de um elemento antes e depois com um determinado view-transition-name. Se houver dois ou mais elementos com um determinado view-transition-name, o DOM será atualizado para o novo estado imediatamente, sem uma transição.

É possível usar vários view-transition-names para transformar vários pares de elementos. Por exemplo, talvez você queira fazer a transição da imagem e do título do produto com transições separadas.

Como há vários itens na visualização em lista, não é possível dar a todos eles o mesmo view-transition-name. Isso pode ser resolvido de duas maneiras em um SPA.

  1. Página de detalhes dinâmica: atribua a cada item na página de lista um view-transition-name exclusivo e aplique esse nome dinamicamente ao elemento correspondente na página de detalhes quando o item da lista for selecionado, como mostrado aqui.

    /* In the list view, give each */
    #product-1 { view-transition-name: p1 }
    #product-2 { view-transition-name: p2 }
    #product-3 { view-transition-name: p3 }
    
    function updateDOM(clickedTransitionName){
      const hero = document.getElementById("hero");
      hero.style.viewTransitionName = clickedTransitionName;
    }
    
  2. Item de lista dinâmico: atribua um view-transition-name ao elemento na página de detalhes e aplique esse nome ao item na página de lista quando ele for selecionado. Remova o view-transition-name do item na página de lista ao retornar a ela.

O elemento #hero na página de detalhes e o elemento .thumbnail selecionado na página de lista compartilham um view-transition-name.

#hero{
  view-transition-name: hero;
}
.thumbnail.selected {
  view-transition-name: hero;
}

Quando uma miniatura é clicada, precisamos preparar a visualização em lista atribuindo o view-transition-name usando o seletor de classe .selected e fazendo as mudanças no DOM antes de iniciar a transição.

Em seguida, chame document.startViewTransition() e aplique as mudanças para fazer a transição da página da visualização detalhada para a visualização em lista.

Depois de voltar para a visualização em lista, limpe as classes de transição de visualização para evitar erros na próxima navegação. É possível realizar essa limpeza depois que a promessa finished da transição for resolvida.

// Function called when a thumbnail is clicked
function goFromListToDetail(e){
  e.currentTarget.classList.add("selected");
  const hero = document.getElementById("hero");
  const bgColor = getComputedStyle(e.currentTarget).backgroundColor;
  hero.style.background = bgColor;

  // Trigger the transition, checking for support
  if (!document.startViewTransition) {
    document.body.classList.add("detail");
    // MANDATORY Accessibility Routing: Route focus to the newly revealed heading to announce context and preserve logical tab flow
    document.getElementById("detail-heading")?.focus();
    return; // MANDATORY: End function execution if view transitions are not supported.  
  }
  const transition = document.startViewTransition(() => {
    document.body.classList.add("detail");
  });
  // MANDATORY Accessibility Routing: Route focus after the view transition resolves
  transition.finished.finally(() => {
    document.getElementById("detail-heading")?.focus();
  });
}

// Function called when navigating from detail back to list view
function goFromDetailToList() {
  if (!document.startViewTransition) {
    document.body.classList.remove("detail");
    document.getElementById("list-heading")?.focus();
    return;
  }
  const transition = document.startViewTransition(() => {
    document.body.classList.remove("detail");
  });
  // Clean up the list view and route focus
  transition.finished.finally(() => {
    // Route focus back to list view
    document.getElementById("list-heading")?.focus();
    // Remove selected classList to remove view-transition-names
    document.querySelectorAll(".selected").forEach(
      (element) => {
        element.classList.remove("selected");
      },
    );
  });
}

O método escolhido depende do caso de uso. O item de lista dinâmico requer menos CSS repetido, mas mais limpeza manual de JavaScript.

Etapa 3: corrigir o "alongamento" da proporção

Por padrão, o navegador faz um cross-fade entre os snapshots antigos e novos em um grupo que se estende para caber os dois. Se você estiver fazendo a transição de texto, defina a largura do elemento do texto como fit-content nas visualizações antiga e nova para que a proporção do elemento em transição seja estável.

#list-page .title {
  width: fit-content;
}

#detail-page #title {
  width: fit-content;
}

Se você estiver fazendo a transição de elementos que mudam a proporção, talvez seja necessário definir a altura dos pseudoelementos antigos e novos como 100% do pseudoelemento ::view-transition-pair().

::view-transition-old(hero),
::view-transition-new(hero){
  height: 100%;
}

Os pseudoelementos são snapshots dos elementos ativos. Portanto, você também pode usar as declarações object-fit e object-position para ter mais controle sobre o efeito de transição.

Práticas recomendadas

  • NÃO especifique muitas transições. Use elementos compartilhados apenas para o conteúdo principal que o usuário está acompanhando ativamente (por exemplo, imagens principais, títulos).
  • REMOVA os valores temporários de view-transition-name após o término da transição para evitar efeitos colaterais em transições futuras.
  • NÃO faça a transição de elementos com animações ativas. As transições de visualização operam em snapshots, então todas as animações vão parecer pausadas durante a transição.
  • RECOMENDADO: respeite as preferências do usuário para reduzir o movimento usando a consulta de mídia prefers-reduced-motion.
  • Roteamento de acessibilidade OBRIGATÓRIO: as transições de visualização transformam os layouts de página dinamicamente, mas não gerenciam o foco programático. Se o foco permanecer em um elemento que foi ocultado ou removido durante a transição, ele será abandonado, deixando os usuários de teclado e tecnologia adaptativa sem contexto. Mude o foco de forma programática para um título de página ou contêiner de visualização atualizado (usando tabindex="-1") imediatamente após as atualizações do DOM ou quando a promessa finished da transição de visualização for resolvida.
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Estratégias de fallback

A API View Transitions foi projetada para aprimoramento progressivo. Os navegadores que não oferecem suporte a ela simplesmente executam a atualização do DOM imediatamente, sem animação.

function navigate(){
  if (!document.startViewTransition) {
    // Fallback: Just update the DOM
    updateDOM();
  } else {
    document.startViewTransition(() => updateDOM());
  }
}