Publicado em 17 de agosto de 2021, Última atualização: 25 de setembro de 2024
Quando uma transição de visualização é executada em um único documento, ela é chamada de transição de visualização do mesmo documento. Isso normalmente ocorre em aplicativos de página única (SPAs) em que o JavaScript é usado para atualizar o DOM. As transições de visualização do mesmo documento são compatíveis com o Chrome a partir da versão 111.
Para acionar uma transição de visualização no mesmo documento, chame document.startViewTransition
:
function handleClick(e) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow();
return;
}
// With a View Transition:
document.startViewTransition(() => updateTheDOMSomehow());
}
Quando invocado, o navegador captura automaticamente snapshots de todos os elementos que têm uma propriedade CSS view-transition-name
declarada.
Em seguida, ele executa o callback transmitido que atualiza o DOM e depois faz snapshots do novo estado.
Esses instantâneos são então organizados em uma árvore de pseudoelementos e animados usando o poder das animações CSS. Pares de snapshots do estado antigo e novo fazem a transição suave da posição e do tamanho antigos para o novo, enquanto o conteúdo é cruzado. Se quiser, use CSS para personalizar as animações.
A transição padrão: transição suave
A transição de visualização padrão é um cross-fade, então ela serve como uma boa introdução à API:
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
// With a transition:
document.startViewTransition(() => updateTheDOMSomehow(data));
}
Onde updateTheDOMSomehow
muda o DOM para o novo estado. Isso pode ser feito da maneira que você quiser. Por exemplo, é possível adicionar ou remover elementos, mudar nomes de classes ou estilos.
E assim, as páginas têm um cross-fade:
Um crossfade não é tão impressionante. Felizmente, as transições podem ser personalizadas, mas primeiro você precisa entender como esse crossfade básico funciona.
Como essas transições funcionam
Vamos atualizar o exemplo de código anterior.
document.startViewTransition(() => updateTheDOMSomehow(data));
Quando .startViewTransition()
é chamado, a API captura o estado atual da página. Isso inclui tirar um snapshot.
Quando a operação for concluída, o callback transmitido para .startViewTransition()
será chamado. É aí que o DOM é alterado. Em seguida, a API captura o novo estado da página.
Depois que o novo estado é capturado, a API constrói uma árvore de pseudoelementos assim:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
O ::view-transition
fica em uma sobreposição, sobre tudo o que está na página. Isso é útil se você quiser definir uma cor de plano de fundo para a transição.
::view-transition-old(root)
é uma captura de tela da visualização antiga, e ::view-transition-new(root)
é uma representação ao vivo da nova. Ambos são renderizados como "conteúdo substituído" de CSS (como um <img>
).
A visualização antiga é animada de opacity: 1
para opacity: 0
, enquanto a nova é animada de opacity: 0
para opacity: 1
, criando um crossfade.
Toda a animação é realizada usando animações CSS, para que possam ser personalizadas com CSS.
Personalizar a transição
Todos os pseudoelementos de transição de visualização podem ser segmentados com CSS e, como as animações são definidas com CSS, é possível modificá-las usando as propriedades de animação CSS existentes. Exemplo:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 5s;
}
Com essa mudança, o desbotamento agora é muito lento:
Certo, isso ainda não é impressionante. Em vez disso, o código abaixo implementa a transição de eixo compartilhado do Material Design:
@keyframes fade-in {
from { opacity: 0; }
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes slide-from-right {
from { transform: translateX(30px); }
}
@keyframes slide-to-left {
to { transform: translateX(-30px); }
}
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
Confira o resultado:
Fazer a transição de vários elementos
Na demonstração anterior, toda a página está envolvida na transição do eixo compartilhado. Isso funciona para a maior parte da página, mas não parece certo para o título, já que ele desliza para fora apenas para deslizar de volta.
Para evitar isso, extraia o cabeçalho do restante da página para que ele possa ser animado separadamente. Para fazer isso, atribua um view-transition-name
ao elemento.
.main-header {
view-transition-name: main-header;
}
O valor de view-transition-name
pode ser o que você quiser, exceto none
, que significa que não há nome de transição. Ele é usado para identificar o elemento de forma exclusiva durante a transição.
E o resultado disso:
Agora, o cabeçalho permanece no lugar e esmaece.
Essa declaração CSS fez com que a árvore de pseudoelementos mudasse:
::view-transition
├─ ::view-transition-group(root)
│ └─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root)
│ └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
└─ ::view-transition-image-pair(main-header)
├─ ::view-transition-old(main-header)
└─ ::view-transition-new(main-header)
Agora há dois grupos de transição. Um para o cabeçalho e outro para o restante. Eles podem ser segmentados de forma independente com CSS e receber transições diferentes. No entanto, nesse caso, main-header
foi deixado com a transição padrão, que é um cross-fade.
A transição padrão não é apenas um crossfade, o ::view-transition-group
também faz transições:
- Posicionar e transformar (usando um
transform
) - Largura
- Altura
Isso não importou até agora, já que o cabeçalho tem o mesmo tamanho e posição de ambos os lados da mudança do DOM. Mas você também pode extrair o texto no cabeçalho:
.main-header-text {
view-transition-name: main-header-text;
width: fit-content;
}
fit-content
é usado para que o elemento tenha o tamanho do texto, em vez de esticar até a largura restante. Sem isso, a seta para trás reduz o tamanho do elemento de texto do cabeçalho, em vez do mesmo tamanho em ambas as páginas.
Agora temos três partes para brincar:
::view-transition
├─ ::view-transition-group(root)
│ └─ …
├─ ::view-transition-group(main-header)
│ └─ …
└─ ::view-transition-group(main-header-text)
└─ …
Mas, novamente, vamos usar os padrões:
Agora o texto do título desliza para o lado para abrir espaço para o botão "Voltar".
Animar vários pseudoelementos da mesma forma com view-transition-class
Compatibilidade com navegadores
Digamos que você tenha uma transição de visualização com vários cards, mas também um título na página. Para animar todos os cards, exceto o título, você precisa escrever um seletor que segmente todos eles.
h1 {
view-transition-name: title;
}
::view-transition-group(title) {
animation-timing-function: ease-in-out;
}
#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
…
#card20 { view-transition-name: card20; }
::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),
…
::view-transition-group(card20) {
animation-timing-function: var(--bounce);
}
Você tem 20 elementos? São 20 seletores que você precisa escrever. Adicionando um novo elemento? Em seguida, você também precisa aumentar o seletor que aplica os estilos de animação. Não é exatamente escalonável.
O view-transition-class
pode ser usado nos pseudoelementos de transição de visualização para aplicar a mesma regra de estilo.
#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }
…
#card20 { view-transition-name: card20; }
#cards-wrapper > div {
view-transition-class: card;
}
html::view-transition-group(.card) {
animation-timing-function: var(--bounce);
}
O exemplo de cards a seguir usa o snippet de CSS anterior. Todos os cards, incluindo os adicionados recentemente, têm o mesmo tempo aplicado com um seletor: html::view-transition-group(.card)
.
Depurar transições
Como as transições de visualização são criadas com base em animações CSS, o painel Animations do Chrome DevTools é ótimo para depurar transições.
Usando o painel Animations, você pode pausar a próxima animação e depois passar para frente e para trás. Durante esse processo, os pseudoelementos de transição podem ser encontrados no painel Elementos.
Os elementos de transição não precisam ser o mesmo elemento DOM
Até agora, usamos view-transition-name
para criar elementos de transição separados para o cabeçalho e o texto dele. Conceitualmente, eles são o mesmo elemento antes e depois da mudança do DOM, mas é possível criar transições em que isso não acontece.
Por exemplo, a incorporação de vídeo principal pode receber um view-transition-name
:
.full-embed {
view-transition-name: full-embed;
}
Assim, ao clicar na miniatura, ela pode receber o mesmo view-transition-name
apenas durante a transição:
thumbnail.onclick = async () => {
thumbnail.style.viewTransitionName = 'full-embed';
document.startViewTransition(() => {
thumbnail.style.viewTransitionName = '';
updateTheDOMSomehow();
});
};
Resultado:
A miniatura agora faz a transição para a imagem principal. Embora sejam elementos conceitualmente (e literalmente) diferentes, a API de transição os trata como a mesma coisa porque eles compartilham o mesmo view-transition-name
.
O código real dessa transição é um pouco mais complicado do que o exemplo anterior, porque também processa a transição de volta para a página de miniaturas. Consulte a fonte para conferir a implementação completa.
Transições personalizadas de entrada e saída
Confira este exemplo:
A barra lateral faz parte da transição:
.sidebar {
view-transition-name: sidebar;
}
Mas, ao contrário do cabeçalho no exemplo anterior, a barra lateral não aparece em todas as páginas. Se ambos os estados tiverem a barra lateral, os pseudoelementos de transição vão ficar assim:
::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
└─ ::view-transition-image-pair(sidebar)
├─ ::view-transition-old(sidebar)
└─ ::view-transition-new(sidebar)
No entanto, se a barra lateral estiver apenas na nova página, o pseudoelemento ::view-transition-old(sidebar)
não estará lá. Como não há uma imagem "antiga" para a barra lateral, o par de imagens terá apenas um ::view-transition-new(sidebar)
. Da mesma forma, se a barra lateral estiver apenas na página antiga, o par de imagens terá apenas um ::view-transition-old(sidebar)
.
Na demonstração anterior, a barra lateral faz transições diferentes dependendo se ela está entrando, saindo ou presente nos dois estados. Ele entra deslizando da direita e esmaece, sai deslizando para a direita e esmaece e permanece no lugar quando está presente nos dois estados.
Para criar transições de entrada e saída específicas, use a pseudoclasse :only-child
para direcionar os pseudoelementos antigos ou novos quando eles forem o único filho no par de imagens:
/* Entry transition */
::view-transition-new(sidebar):only-child {
animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
/* Exit transition */
::view-transition-old(sidebar):only-child {
animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}
Nesse caso, não há uma transição específica para quando a barra lateral está presente nos dois estados, já que o padrão é perfeito.
Atualizações assíncronas de DOM e aguardando conteúdo
O callback transmitido para .startViewTransition()
pode retornar uma promessa, o que permite atualizações assíncronas do DOM e a espera por conteúdo importante.
document.startViewTransition(async () => {
await something;
await updateTheDOMSomehow();
await somethingElse;
});
A transição não será iniciada até que a promessa seja cumprida. Durante esse período, a página fica congelada, por isso os atrasos devem ser mantidos no mínimo. Especificamente, as buscas de rede precisam ser feitas antes de chamar .startViewTransition()
, enquanto a página ainda está totalmente interativa, em vez de fazê-las como parte do callback .startViewTransition()
.
Se você decidir esperar até que as imagens ou fontes estejam prontas, use um tempo limite agressivo:
const wait = ms => new Promise(r => setTimeout(r, ms));
document.startViewTransition(async () => {
updateTheDOMSomehow();
// Pause for up to 100ms for fonts to be ready:
await Promise.race([document.fonts.ready, wait(100)]);
});
No entanto, em alguns casos, é melhor evitar o atraso e usar o conteúdo que você já tem.
Aproveite ao máximo o conteúdo que você já tem
No caso em que a miniatura faz a transição para uma imagem maior:
A transição padrão é a transição cruzada, o que significa que a miniatura pode estar em transição cruzada com uma imagem completa que ainda não foi carregada.
Uma maneira de lidar com isso é esperar a imagem completa carregar antes de iniciar a transição. O ideal é fazer isso antes de chamar .startViewTransition()
, para que a página permaneça interativa e um indicador de carregamento possa ser mostrado para indicar ao usuário que as coisas estão sendo carregadas. Mas, neste caso, há uma maneira melhor:
::view-transition-old(full-embed),
::view-transition-new(full-embed) {
/* Prevent the default animation,
so both views remain opacity:1 throughout the transition */
animation: none;
/* Use normal blending,
so the new view sits on top and obscures the old view */
mix-blend-mode: normal;
}
Agora a miniatura não desaparece, ela fica abaixo da imagem completa. Isso significa que, se a nova visualização não tiver sido carregada, a miniatura ficará visível durante a transição. Isso significa que a transição pode começar imediatamente, e a imagem completa pode ser carregada em seu próprio tempo.
Isso não funcionaria se a nova visualização tivesse transparência, mas, neste caso, sabemos que não tem, então podemos fazer essa otimização.
Processar mudanças na proporção
Convenientemente, todas as transições até agora foram para elementos com a mesma proporção, mas nem sempre será assim. E se a miniatura for 1:1 e a imagem principal for 16:9?
Na transição padrão, o grupo é animado do tamanho anterior para o tamanho posterior. As visualizações antigas e novas têm 100% da largura do grupo e altura automática, o que significa que elas mantêm a proporção, independentemente do tamanho do grupo.
Esse é um bom padrão, mas não é o que é esperado nesse caso. Então:
::view-transition-old(full-embed),
::view-transition-new(full-embed) {
/* Prevent the default animation,
so both views remain opacity:1 throughout the transition */
animation: none;
/* Use normal blending,
so the new view sits on top and obscures the old view */
mix-blend-mode: normal;
/* Make the height the same as the group,
meaning the view size might not match its aspect-ratio. */
height: 100%;
/* Clip any overflow of the view */
overflow: clip;
}
/* The old view is the thumbnail */
::view-transition-old(full-embed) {
/* Maintain the aspect ratio of the view,
by shrinking it to fit within the bounds of the element */
object-fit: contain;
}
/* The new view is the full image */
::view-transition-new(full-embed) {
/* Maintain the aspect ratio of the view,
by growing it to cover the bounds of the element */
object-fit: cover;
}
Isso significa que a miniatura permanece no centro do elemento à medida que a largura aumenta, mas a imagem completa "desenquadra" à medida que faz a transição de 1:1 para 16:9.
Para mais informações, consulte Transições de visualização: como lidar com mudanças na proporção.
Usar consultas de mídia para mudar as transições para diferentes estados do dispositivo
Talvez você queira usar transições diferentes em dispositivos móveis e computadores, como neste exemplo que mostra uma transição completa do lado em dispositivos móveis, mas uma transição mais sutil em computadores:
Isso pode ser feito usando consultas de mídia regulares:
/* Transitions for mobile */
::view-transition-old(root) {
animation: 300ms ease-out both full-slide-to-left;
}
::view-transition-new(root) {
animation: 300ms ease-out both full-slide-from-right;
}
@media (min-width: 500px) {
/* Overrides for larger displays.
This is the shared axis transition from earlier in the article. */
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
}
Você também pode mudar os elementos a que você atribui um view-transition-name
dependendo das consultas de mídia correspondentes.
Reagir à preferência de "movimento reduzido"
Os usuários podem indicar que preferem a redução de movimento no sistema operacional, e essa preferência é exposta no CSS.
Você pode impedir as transições para esses usuários:
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
No entanto, a preferência por "movimento reduzido" não significa que o usuário queira nenhum movimento. Em vez do snippet anterior, você pode escolher uma animação mais sutil, mas que ainda expresse a relação entre os elementos e o fluxo de dados.
Processar vários estilos de transição de visualização com tipos de transição de visualização
Compatibilidade com navegadores
Às vezes, uma transição de uma visualização específica para outra precisa ter uma transição personalizada. Por exemplo, ao acessar a página seguinte ou anterior em uma sequência de paginação, você pode deslizar o conteúdo em uma direção diferente, dependendo se você está indo para uma página mais alta ou mais baixa da sequência.
Para isso, você pode usar os tipos de transição de visualização, que permitem atribuir um ou mais tipos a uma transição do Active View. Por exemplo, ao fazer a transição para uma página mais alta em uma sequência de paginação, use o tipo forwards
. Ao ir para uma página mais baixa, use o tipo backwards
. Esses tipos só ficam ativos ao capturar ou realizar uma transição, e cada um deles pode ser personalizado pelo CSS para usar animações diferentes.
Para usar tipos em uma transição de visualização do mesmo documento, transmita types
para o método startViewTransition
. Para permitir isso, document.startViewTransition
também aceita um objeto: update
é a função de callback que atualiza o DOM, e types
é uma matriz com os tipos.
const direction = determineBackwardsOrForwards();
const t = document.startViewTransition({
update: updateTheDOMSomehow,
types: ['slide', direction],
});
Para responder a esses tipos, use o seletor :active-view-transition-type()
. Transmita o type
que você quer segmentar para o seletor. Isso permite manter os estilos de várias transições de visualização separados uns dos outros, sem que as declarações de uma interfiram nas declarações da outra.
Como os tipos só se aplicam ao capturar ou realizar a transição, você pode usar o seletor para definir (ou cancelar) uma view-transition-name
em um elemento somente para a transição de visualização com esse tipo.
/* 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 (using the default root snapshot) */
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;
}
}
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. Os tipos são determinados no clique em que são transmitidos para document.startViewTransition
.
Para segmentar qualquer transição de visualização ativa, independentemente do tipo, use o seletor de pseudoclasse :active-view-transition
.
html:active-view-transition {
…
}
Processar vários estilos de transição de visualização com um nome de classe na raiz da transição de visualização
Às vezes, uma transição de um tipo específico de visualização para outra precisa ter uma transição personalizada. Ou seja, a navegação "voltar" precisa ser diferente da navegação "avançar".
Antes dos tipos de transição, a forma de lidar com esses casos era definir temporariamente um nome de classe na raiz da transição. Ao chamar document.startViewTransition
, essa raiz de transição é o elemento <html>
, acessível usando document.documentElement
no JavaScript:
if (isBackNavigation) {
document.documentElement.classList.add('back-transition');
}
const transition = document.startViewTransition(() =>
updateTheDOMSomehow(data)
);
try {
await transition.finished;
} finally {
document.documentElement.classList.remove('back-transition');
}
Para remover as classes após o término da transição, este exemplo usa transition.finished
, uma promessa que é resolvida quando a transição atinge o estado final. Outras propriedades deste objeto são abordadas na Referência da API.
Agora você pode usar esse nome de classe no CSS para mudar a transição:
/* 'Forward' transitions */
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
animation-name: fade-out, slide-to-right;
}
.back-transition::view-transition-new(root) {
animation-name: fade-in, slide-from-left;
}
Assim como nas consultas de mídia, a presença dessas classes também pode ser usada para mudar quais elementos recebem um view-transition-name
.
Executar transições sem congelar outras animações
Confira esta demonstração de uma posição de transição de vídeo:
Você notou algo errado? Não se preocupe se não tiver feito isso. Aqui, o ritmo é mais lento:
Durante a transição, o vídeo parece congelar e, em seguida, a versão em reprodução aparece. Isso ocorre porque ::view-transition-old(video)
é uma captura de tela da visualização antiga, enquanto ::view-transition-new(video)
é uma imagem ao vivo da nova visualização.
É possível corrigir isso, mas primeiro pergunte a si mesmo se vale a pena. Se você não notou o "problema" quando a transição estava sendo reproduzida na velocidade normal, não se preocupe em mudar.
Se você realmente quiser corrigir isso, não mostre o ::view-transition-old(video)
. Mude diretamente para o ::view-transition-new(video)
. Para isso, substitua os estilos e animações padrão:
::view-transition-old(video) {
/* Don't show the frozen old view */
display: none;
}
::view-transition-new(video) {
/* Don't fade the new view in */
animation: none;
}
Pronto.
Agora o vídeo é reproduzido durante a transição.
Integração com a API Navigation (e outros frameworks)
As transições de visualização são especificadas de modo que possam ser integradas a outros frameworks ou bibliotecas. Por exemplo, se o aplicativo de página única (SPA) estiver usando um roteador, você poderá ajustar o mecanismo de atualização do roteador para atualizar o conteúdo usando uma transição de visualização.
No snippet de código a seguir, retirado da demonstração de paginação, o gerenciador de interceptação da API Navigation é ajustado para chamar document.startViewTransition
quando as transições de visualização são compatíveis.
navigation.addEventListener("navigate", (e) => {
// Don't intercept if not needed
if (shouldNotIntercept(e)) return;
// Intercept the navigation
e.intercept({
handler: async () => {
// Fetch the new content
const newContent = await fetchNewContent(e.destination.url, {
signal: e.signal,
});
// The UA does not support View Transitions, or the UA
// already provided a Visual Transition by itself (e.g. swipe back).
// In either case, update the DOM directly
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
// Update the content using a View Transition
const t = document.startViewTransition(() => {
setContent(newContent);
});
}
});
});
Alguns navegadores, mas não todos, oferecem a própria transição quando o usuário realiza um gesto de deslizar para navegar. Nesse caso, não acione sua própria transição de visualização, porque isso pode gerar uma experiência ruim ou confusa para o usuário. O usuário vai notar duas transições, uma fornecida pelo navegador e outra por você, sendo executadas em sucessão.
Portanto, é recomendável impedir que uma transição de visualização seja iniciada quando o navegador fornece a própria transição visual. Para fazer isso, verifique o valor da propriedade hasUAVisualTransition
da instância NavigateEvent
. A propriedade é definida como true
quando o navegador fornece uma transição visual. Essa propriedade hasUIVisualTransition
também existe em instâncias PopStateEvent
.
No snippet anterior, a verificação que determina se a transição da visualização deve ser executada leva essa propriedade em consideração. Quando não há suporte para transições de visualização no mesmo documento ou quando o navegador já forneceu a própria transição, a transição de visualização é ignorada.
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
Na gravação a seguir, o usuário desliza para voltar à página anterior. A captura à esquerda não inclui uma verificação da sinalização hasUAVisualTransition
. A gravação à direita inclui a verificação, pulando a transição de visualização manual porque o navegador fornecia uma transição visual.
Como animar com JavaScript
Até agora, todas as transições foram definidas usando CSS, mas às vezes o CSS não é suficiente:
Algumas partes dessa transição não podem ser alcançadas apenas com CSS:
- A animação começa no local do clique.
- A animação termina com o círculo tendo um raio até o canto mais distante. No entanto, esperamos que isso seja possível com o CSS no futuro.
Felizmente, é possível criar transições usando a API Web Animation.
let lastClick;
addEventListener('click', event => (lastClick = event));
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
// Get the click position, or fallback to the middle of the screen
const x = lastClick?.clientX ?? innerWidth / 2;
const y = lastClick?.clientY ?? innerHeight / 2;
// Get the distance to the furthest corner
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
);
// With a transition:
const transition = document.startViewTransition(() => {
updateTheDOMSomehow(data);
});
// Wait for the pseudo-elements to be created:
transition.ready.then(() => {
// Animate the root's new view
document.documentElement.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
],
},
{
duration: 500,
easing: 'ease-in',
// Specify which pseudo-element to animate
pseudoElement: '::view-transition-new(root)',
}
);
});
}
Este exemplo usa transition.ready
, uma promessa que é resolvida quando os pseudoelementos de transição são criados. Outras propriedades deste objeto são abordadas na referência da API.
Transições como um aprimoramento
A API View Transition foi projetada para "encapsular" uma alteração do DOM e criar uma transição para ela. No entanto, a transição precisa ser tratada como uma melhoria, ou seja, o app não pode entrar em um estado de "erro" se a mudança do DOM for bem-sucedida, mas a transição falhar. O ideal é que a transição não falhe, mas, se isso acontecer, ela não pode interromper o restante da experiência do usuário.
Para tratar as transições como uma melhoria, não use promessas de transição de uma maneira que cause a falha do app.
async function switchView(data) { // Fallback for browsers that don't support this API: if (!document.startViewTransition) { await updateTheDOM(data); return; } const transition = document.startViewTransition(async () => { await updateTheDOM(data); }); await transition.ready; document.documentElement.animate( { clipPath: [`inset(50%)`, `inset(0)`], }, { duration: 500, easing: 'ease-in', pseudoElement: '::view-transition-new(root)', } ); }
O problema com esse exemplo é que switchView()
será rejeitado se a transição não puder alcançar um estado ready
, mas isso não significa que a visualização não foi alterada. O DOM pode ter sido atualizado, mas houve view-transition-name
s duplicados, então a transição foi ignorada.
Em vez disso, faça o seguinte:
async function switchView(data) { // Fallback for browsers that don't support this API: if (!document.startViewTransition) { await updateTheDOM(data); return; } const transition = document.startViewTransition(async () => { await updateTheDOM(data); }); animateFromMiddle(transition); await transition.updateCallbackDone; } async function animateFromMiddle(transition) { try { await transition.ready; document.documentElement.animate( { clipPath: [`inset(50%)`, `inset(0)`], }, { duration: 500, easing: 'ease-in', pseudoElement: '::view-transition-new(root)', } ); } catch (err) { // You might want to log this error, but it shouldn't break the app } }
Este exemplo usa transition.updateCallbackDone
para aguardar a atualização do DOM e rejeitá-la em caso de falha. switchView
não vai mais rejeitar se a transição falhar, será resolvida quando a atualização do DOM for concluída e vai ser rejeitada se falhar.
Se você quiser que switchView
seja resolvido quando a nova visualização for "resolvida", ou seja, quando qualquer transição animada for concluída ou pulada para o final, substitua transition.updateCallbackDone
por transition.finished
.
Não é um polyfill, mas…
Esse não é um recurso fácil de usar. No entanto, essa função auxiliar facilita muito as coisas em navegadores que não oferecem suporte a transições de visualização:
function transitionHelper({
skipTransition = false,
types = [],
update,
}) {
const unsupported = (error) => {
const updateCallbackDone = Promise.resolve(update()).then(() => {});
return {
ready: Promise.reject(Error(error)),
updateCallbackDone,
finished: updateCallbackDone,
skipTransition: () => {},
types,
};
}
if (skipTransition || !document.startViewTransition) {
return unsupported('View Transitions are not supported in this browser');
}
try {
const transition = document.startViewTransition({
update,
types,
});
return transition;
} catch (e) {
return unsupported('View Transitions with types are not supported in this browser');
}
}
E pode ser usado assim:
function spaNavigate(data) {
const types = isBackNavigation ? ['back-transition'] : [];
const transition = transitionHelper({
update() {
updateTheDOMSomehow(data);
},
types,
});
// …
}
Em navegadores que não oferecem suporte a transições de visualização, o updateDOM
ainda é chamado, mas não há uma transição animada.
Você também pode fornecer alguns classNames
para adicionar ao <html>
durante a transição, facilitando a mudança da transição dependendo do tipo de navegação.
Também é possível transmitir true
para skipTransition
se você não quiser uma animação, mesmo em navegadores com suporte a transições de visualização. Isso é útil se o site tiver uma preferência do usuário para desativar transições.
Como trabalhar com frameworks
Se você estiver trabalhando com uma biblioteca ou framework que abstrai as mudanças do DOM, a parte complicada é saber quando a mudança do DOM é concluída. Confira um conjunto de exemplos que usam o auxiliar acima em várias estruturas.
- Reagir: a chave aqui é
flushSync
, que aplica um conjunto de mudanças de estado de forma síncrona. Sim, há um grande aviso sobre o uso dessa API, mas Dan Abramov garante que ela é adequada neste caso. Como de costume com o React e o código assíncrono, ao usar as várias promessas retornadas porstartViewTransition
, verifique se o código está sendo executado com o estado correto. - Vue.js: a chave aqui é
nextTick
, que é atendida depois que o DOM é atualizado. - Svelte: muito semelhante ao Vue, mas o método para aguardar a próxima mudança é
tick
. - Lit: a chave aqui é a promessa
this.updateComplete
nos componentes, que é cumprida quando o DOM é atualizado. - Angular: a chave aqui é
applicationRef.tick
, que limpa as mudanças pendentes do DOM. A partir da versão 17 do Angular, é possível usarwithViewTransitions
que vem com@angular/router
.
Referência da API
const viewTransition = document.startViewTransition(update)
Inicie uma nova
ViewTransition
.update
é uma função chamada quando o estado atual do documento é capturado.Em seguida, quando a promessa retornada por
updateCallback
é cumprida, a transição começa no próximo frame. Se a promessa retornada porupdateCallback
for rejeitada, a transição será abandonada.const viewTransition = document.startViewTransition({ update, types })
Inicia uma nova
ViewTransition
com os tipos especificadosO
update
é chamado quando o estado atual do documento é capturado.O
types
define os tipos ativos para a transição ao capturar ou executar a transição. Ele está vazio no início. ConsulteviewTransition.types
mais abaixo para mais informações.
Membros de instância de ViewTransition
:
viewTransition.updateCallbackDone
Uma promessa que será atendida quando a promessa retornada por
updateCallback
for atendida ou rejeitada quando for rejeitada.A API View Transition agrupa uma mudança de DOM e cria uma transição. No entanto, às vezes você não se importa com o sucesso ou o fracasso da animação de transição. Você só quer saber se e quando a mudança do DOM acontece.
updateCallbackDone
é para esse caso de uso.viewTransition.ready
Uma promessa que é cumprida quando os pseudoelementos da transição são criados e a animação está prestes a começar.
Ela será rejeitada se a transição não puder começar. Isso pode ser devido a uma configuração incorreta, como
view-transition-name
s duplicados, ou seupdateCallback
retornar uma promessa rejeitada.Isso é útil para animar os pseudoelementos de transição com JavaScript.
viewTransition.finished
uma promessa que será atendida quando o estado final estiver totalmente visível e interativo para o usuário.
Ele só é rejeitado se
updateCallback
retornar uma promessa rejeitada, o que indica que o estado final não foi criado.Caso contrário, se uma transição não começar ou for ignorada durante a transição, o estado final ainda será alcançado, então
finished
será atendido.viewTransition.types
Um objeto semelhante a
Set
que contém os tipos de transição de visualização ativa. Para manipular as entradas, use os métodos de instânciaclear()
,add()
edelete()
.Para responder a um tipo específico no CSS, use o seletor de pseudoclasse
:active-view-transition-type(type)
na raiz da transição.Os tipos são limpos automaticamente quando a transição de visualização é concluída.
viewTransition.skipTransition()
Pule a parte de animação da transição.
Essa ação não pula a chamada de
updateCallback
, já que a mudança do DOM é separada da transição.
Estilo padrão e referência de transição
::view-transition
- O pseudoelemento raiz que preenche a viewport e contém cada
::view-transition-group
. ::view-transition-group
Posicionado de forma absoluta.
Transições
width
eheight
entre os estados "antes" e "depois".Faz uma transição
transform
entre o quadrado de espaço da janela de visualização "antes" e "depois".::view-transition-image-pair
Absolutamente posicionado para preencher o grupo.
Tem
isolation: isolate
para limitar o efeito domix-blend-mode
nas visualizações antigas e novas.::view-transition-new
e::view-transition-old
Totalmente posicionado no canto superior esquerdo do wrapper.
Preenche 100% da largura do grupo, mas tem uma altura automática, então ele mantém a proporção em vez de preencher o grupo.
Tem
mix-blend-mode: plus-lighter
para permitir um cross-fade real.A visualização antiga muda de
opacity: 1
paraopacity: 0
. A nova visualização transita deopacity: 0
paraopacity: 1
.
Feedback
O feedback dos desenvolvedores é sempre bem-vindo. Para isso, registre um problema com o grupo de trabalho de CSS no GitHub (em inglês) com sugestões e perguntas. Adicione o prefixo [css-view-transitions]
ao seu problema.
Se você encontrar um bug, registre um bug do Chromium.