Transições de visualização do mesmo documento para aplicativos de página única

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. A partir do Chrome 111, as transições de visualização de um mesmo documento estão disponíveis no Chrome.

Para acionar uma transição de visualização do 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 neles.

Em seguida, ele executa o retorno de chamada transmitido que atualiza o DOM. Depois disso, ele captura 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 local, enquanto os crossfades de conteúdo. Se desejar, você pode usar CSS para personalizar as animações.


A transição padrão: cross-fade

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

Em que 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 e alterar nomes de classes ou estilos.

Assim, as páginas fazem a transição cruzada:

O cross-fade padrão. Demonstração mínima. Origem.

Certo, o cross-fade não é tão impressionante. Felizmente, as transições podem ser personalizadas, mas primeiro você precisa entender como esse cross-fade básico funcionou.


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.

Depois de concluído, o callback transmitido para .startViewTransition() é 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 cria uma árvore de pseudoelementos como esta:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

A ::view-transition fica em sobreposição, sobre todo o resto da 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 visualização. 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 visualização é animada de opacity: 0 para opacity: 1, criando um cross-fade.

Toda a animação é executada usando animações CSS. Por isso, elas podem 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 única mudança, o esmaecimento agora está bem lento:

Cruzamento longo. Demonstração mínima. Origem.

Certo, isso ainda não é impressionante. Em vez disso, o código abaixo implementa a transição de eixo compartilhado do Material Design (link em inglês):

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

E este é o resultado:

Transição de eixo compartilhado. Demonstração mínima. Origem.

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 correto para o título, uma vez que ele desliza para fora apenas para deslizar de volta de novo.

Para evitar isso, você pode extrair 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á um nome de transição). Ele é usado para identificar o elemento de forma exclusiva durante a transição.

O resultado disso:

Transição de eixo compartilhado com cabeçalho fixo. Demonstração mínima. Origem.

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 existem dois grupos de transição. uma para o cabeçalho e outra para o resto. Elas podem ser segmentadas de maneira independente com CSS e em transições diferentes. Neste caso, main-header ficou com a transição padrão, que é um cross-fade.

Bom, a transição padrão não é apenas um cross fade, a ::view-transition-group também faz a transição:

  • 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 do 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.

Então, agora temos três partes:

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

Mas, novamente, apenas com os padrões:

Texto do cabeçalho deslizante. Demonstração mínima. Origem.

Agora, o texto do título desliza um pouco para criar espaço para o botão "Voltar".


Animar vários pseudoelementos da mesma forma com view-transition-class

Compatibilidade com navegadores

  • 125
  • 125
  • x
  • x

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

Tem 20 elementos? São 20 seletores que você precisa criar. Adicionando um novo elemento? Depois, 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 recém-adicionados, têm o mesmo tempo aplicado com um seletor: html::view-transition-group(.card).

Gravação da demonstração de cards. Usando view-transition-class, o mesmo animation-timing-function é aplicado a todos os cards, exceto os adicionados ou removidos.

Depurar transições

Como as transições de visualização são criadas com base em animações CSS, o painel Animations no Chrome DevTools é ótimo para depurar transições.

Com o painel Animações, é possível pausar a próxima animação e, em seguida, deslizar para frente e para trás na animação. Durante esse processo, os pseudoelementos de transição podem ser encontrados no painel Elementos.

Depurar transições de visualização com o Chrome DevTools

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 consistem no mesmo elemento antes e depois da alteração do DOM, mas é possível criar transições quando esse não for o caso.

Por exemplo, a incorporação do 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:

Transição de um elemento para outro. Demonstração mínima. Origem.

A miniatura é transferida para a imagem principal. Embora sejam conceitualmente (e literalmente) elementos diferentes, a API de transição os trata como a mesma coisa, porque compartilham o mesmo view-transition-name.

O código real dessa transição é um pouco mais complicado do que o exemplo anterior, já que ele também processa a transição de volta para a página de miniaturas. Consulte a fonte para ver a implementação completa.


Transições personalizadas de entrada e saída

Confira este exemplo:

Entrar e sair da barra lateral. Demonstração mínima. Origem.

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 terão esta aparência:

::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 vai aparecer. 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 transição da barra lateral varia dependendo se ela está entrando, saindo ou presente nos dois estados. Ele entra deslizando da direita para a esquerda, sai deslizando para a direita e desaparecendo e permanece no lugar quando está presente nos dois estados.

Para criar transições específicas de entrada e saída, use a pseudoclasse :only-child para segmentar 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, que permite atualizações assíncronas do DOM e aguarda a preparação do conteúdo importante.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

A transição não será iniciada até que a promessa seja atendida. Durante esse período, a página fica congelada, então os atrasos devem ser mínimos. 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 as imagens ou fontes ficarem 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 esse 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 miniatura em transição para uma imagem maior. Experimente o site de demonstração.

A transição padrão é o cross-fade, o que significa que a miniatura pode ter um esmaecimento cruzado com uma imagem completa ainda não carregada.

Uma forma de lidar com isso é aguardar até que a imagem seja carregada completamente antes de iniciar a transição. O ideal é que isso seja feito antes de chamar .startViewTransition(). Assim, a página permanece interativa e um ícone de carregamento pode ser mostrado para indicar ao usuário que as coisas estão sendo carregadas. Mas, nesse 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 apenas fica abaixo da imagem inteira. 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, para que possamos 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 esse será o caso. E se a miniatura for 1:1 e a imagem principal for 16:9?

Um elemento em transição para outro, com uma proporção alterada. Demonstração mínima. Origem.

Na transição padrão, o grupo é animado do tamanho anterior para o posterior. As visualizações antiga e nova 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. Portanto:

::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 se expande, mas a imagem inteira "é cortada" na transição de 1:1 para 16:9.

Para informações mais detalhadas, consulte Transições de visualização: como lidar com mudanças de proporção.


Usar consultas de mídia para mudar as transições para diferentes estados do dispositivo

É possível usar transições diferentes em dispositivos móveis e computadores, como este exemplo mostra um slide completo da lateral em dispositivos móveis, mas um slide mais sutil no computador:

Transição de um elemento para outro. Demonstração mínima. Origem.

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

Também é possível mudar quais elementos são atribuídos a uma view-transition-name, dependendo das consultas de mídia correspondentes.


Reagir à preferência de "movimento reduzido"

Os usuários podem indicar que preferem a movimentação reduzida pelo sistema operacional, e essa preferência é exposta no CSS.

Você pode optar por impedir transições para esses usuários:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

No entanto, uma preferência por "movimento reduzido" não significa que o usuário não quer 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

Às vezes, a transição de uma visualização específica para outra deve ter uma transição especificamente sob medida. Por exemplo, ao acessar a página seguinte ou a anterior em uma sequência de paginação, talvez você queira deslizar o conteúdo em uma direção diferente, dependendo do direcionamento para uma página superior ou inferior da sequência.

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

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 superior em uma sequência de paginação, use o tipo forwards e, ao acessar uma página inferior, use o tipo backwards. Esses tipos ficam ativos somente ao capturar ou realizar uma transição, e cada tipo pode ser personalizado por meio de CSS para usar diferentes animações.

Para usar tipos em uma transição de visualização de um 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 do Active View, 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 de transição de visualização.

Às vezes, a transição de um tipo específico de visualização para outro deve ter uma transição especificamente sob medida. Ou uma navegação para 'voltar' deve ser diferente de uma navegação para 'avançar'.

Transições diferentes ao voltar. Demonstração mínima. Origem.

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>, que pode ser acessado usando document.documentElement em 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 seu CSS para alterar 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 uma view-transition-name.


Executar transições sem congelar outras animações

Dê uma olhada nesta demonstração de uma posição de transição de vídeo:

Transição de vídeo. Demonstração mínima. Origem.

Você notou algo de errado com ele? Não se preocupe se não fez isso. Aqui, o ritmo é mais lento:

Transição de vídeo, mais lenta. Demonstração mínima. Origem.

Durante a transição, o vídeo parece travar e, em seguida, a versão em reprodução do vídeo aparece gradualmente. 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.

Você pode corrigir isso, mas primeiro, veja se vale a pena corrigir o problema. Se o "problema" não fosse exibido quando a transição estava sendo reproduzida na velocidade normal, eu não me preocuparia em mudá-lo.

Se você realmente quiser corrigir o problema, não mostre o ::view-transition-old(video). Mude diretamente para o ::view-transition-new(video). É possível fazer isso substituindo os estilos e as 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.

Transição de vídeo, mais lenta. Demonstração mínima. Origem.

Agora o vídeo é reproduzido durante a transição.


Como animar com JavaScript

Até agora, todas as transições foram definidas usando CSS, mas às vezes o CSS não é suficiente:

Transição de círculo. Demonstração mínima. Origem.

Algumas partes dessa transição não podem ser realizadas 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. Esperamos que isso seja possível com o CSS no futuro.

Felizmente, você pode 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)',
      }
    );
  });
}

Esse exemplo usa transition.ready, uma promessa que é resolvida depois que 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 deve ser tratada como uma melhoria, ou seja, seu app não poderá entrar em um estado de "erro" se a mudança no DOM tiver êxito, mas a transição falhar. O ideal é que a transição não falhe, mas, se ocorrer, ela não prejudicará o restante da experiência do usuário.

Para tratar as transições como um aprimoramento, tome cuidado para não usar promessas de transição de forma que seu app gere uma falha se a transição falhar.

O que não fazer
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 neste exemplo é que switchView() será rejeitada se a transição não puder alcançar um estado ready, mas isso não significa que a visualização falhou ao alternar. O DOM pode ter sido atualizado, mas havia view-transition-names duplicadas. Por isso, a transição foi ignorada.

Como alternativa:

O que fazer
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 para rejeitar se falhar. switchView não vai mais rejeitar se a transição falhar, será resolvido quando a atualização do DOM for concluída e será rejeitada se falhar.

Se você quiser que switchView seja resolvida quando a nova visualização estiver "resolvida", como em qualquer transição animada que tenha sido 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 com o polyfill. No entanto, essa função auxiliar facilita muito 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 ela pode ser usada 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, updateDOM ainda será chamado, mas não haverá uma transição animada.

Você também pode fornecer classNames para adicionar ao <html> durante a transição, facilitando a mudança de acordo com o tipo de navegação.

Você também pode transmitir true para skipTransition se não quiser uma animação, mesmo em navegadores compatíveis com transições de visualização. Isso é útil se o usuário preferir que o usuário desative as transições.


Como trabalhar com estruturas

Se você está trabalhando com uma biblioteca ou estrutura que abstrai as alterações do DOM, a parte complicada é saber quando a alteração do DOM está 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 isso é adequado nesse caso. Como de costume com o React e o código assíncrono, ao usar as diversas promessas retornadas por startViewTransition, verifique se o código está sendo executado no 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 (link em inglês).
  • Lit: a chave aqui é a promessa this.updateComplete nos componentes, que é atendida após a atualização do DOM.
  • Angular: a chave aqui é applicationRef.tick, que transfere as mudanças pendentes do DOM. A partir da versão 17 do Angular, você pode usar o withViewTransitions que vem com o @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.

Quando a promessa retornada por updateCallback for atendida, a transição começará no frame seguinte. Se a promessa retornada por updateCallback for rejeitada, a transição será abandonada.

const viewTransition = document.startViewTransition({ update, types })

Iniciar um novo ViewTransition com os tipos especificados

O 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. Inicialmente, ele fica vazio. Consulte viewTransition.types mais abaixo para mais informações.

Membros da 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 envolve uma alteração do DOM e cria uma transição. No entanto, às vezes você não se importa com o sucesso ou a falha da animação de transição. Basta saber se e quando a mudança do DOM acontece. updateCallbackDone é para esse caso de uso.

viewTransition.ready

Uma promessa que é atendida 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 ser iniciada. Isso pode ocorrer devido a uma configuração incorreta, como view-transition-names duplicadas, ou se updateCallback retornar uma promessa recusada.

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.

Ela só será rejeitada se updateCallback retornar uma promessa rejeitada, porque isso indica que o estado final não foi criado.

Caso contrário, se uma transição não for iniciada ou for ignorada durante a transição, o estado final ainda será alcançado, então finished será concluído.

viewTransition.types

Um objeto semelhante a Set que contém os tipos da transição do Active View. Para manipular as entradas, use os métodos de instância dele: clear(), add() e delete().

Para responder a um tipo específico no CSS, use o seletor da pseudoclasse :active-view-transition-type(type) na raiz de 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 janela de visualização e contém cada ::view-transition-group.
::view-transition-group

Absolutamente posicionado.

Transições de width e height 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 da mix-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 para manter 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 para opacity: 0. A nova visualização faz a transição de opacity: 0 para opacity: 1.


Feedback

O feedback dos desenvolvedores é sempre bem-vindo. Para fazer isso, 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.