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.
Página de detalhes dinâmica: atribua a cada item na página de lista um
view-transition-nameexclusivo 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; }Item de lista dinâmico: atribua um
view-transition-nameao elemento na página de detalhes e aplique esse nome ao item na página de lista quando ele for selecionado. Remova oview-transition-namedo 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-nameapó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 promessafinishedda 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());
}
}