7 receitas de View Transitions para experimentar
View transitions são realmente muito legais. E não só isso: estão começando a aparecer em todo lugar. A primeira experiência com elas, em sua forma mais simples, costuma ser direta, mas, em muitos casos, envolvem várias partes trabalhando em conjunto.
A melhor maneira de aprender algo novo costuma ser ver o código, usá-lo em prática e, a partir daí, construir em cima. Pensando nisso, este artigo reúne sete receitas de view transitions prontas para experimentar (uma extensão natural para quem já trabalha com animações CSS).
Uma observação rápida antes de prosseguir: no momento em que este texto foi escrito, view transitions já atingiram status Baseline, com suporte em todos os principais navegadores. Ainda assim, certos tipos de animação podem ter variações no suporte entre navegadores, então, vale sempre verificar a compatibilidade antes de levar para produção.
O setup
Antes de qualquer receita, é preciso preparar o terreno. O primeiro passo é habilitar view transitions com a at-rule @view-transition, tanto na página de origem quanto na de destino:
@media (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; types: <transition-type>; }}O <transition-type> é um descritor customizável que funciona como um identificador da animação. Types são basicamente o nome da animação que se dá a uma transição específica — isso evita conflitos quando múltiplas transições operam ao mesmo tempo.
Repare que o bloco está envolvido por uma media query prefers-reduced-motion: no-preference. Nem todo mundo quer movimento nas páginas, e essa é uma preferência que pode ser definida no nível do sistema operacional; respeitá-la é parte de fazer animações acessíveis.
As animações em si são aplicadas via:
html:active-view-transition-type(<transition-type>)::view-transition-old(root) { animation: a-cool-outgoing-animation 1.4s ease forwards;}
html:active-view-transition-type(<transition-type>)::view-transition-new(root) { animation: a-cool-incoming-animation 1.4s ease forwards;}O seletor de pseudo-classe :active-view-transition-type() casa com o type definido. Em um exemplo concreto:
@media (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; types: bounce; }}
/* A página "atual" */html:active-view-transition-type(bounce)::view-transition-old(root) { animation: bounce-in 1.4s ease forwards;}
/* A página para onde se está transicionando */html:active-view-transition-type(bounce)::view-transition-new(root) { animation: bounce-in 1.4s ease forwards;}Com esse esqueleto estabelecido, todas as receitas a seguir seguem o mesmo padrão: definir um type, aplicar uma animação de saída ao ::view-transition-old(root) e uma animação de entrada ao ::view-transition-new(root).
Pixelate dissolve
Esse efeito é parecido com um cross-fade simples, mas com um borrão degradando a transição: a página de saída desfoca e some, enquanto a nova entra voltando do desfoque para o estado nítido.
@media (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; types: pixelate-dissolve; }}
html:active-view-transition-type(pixelate-dissolve)::view-transition-old(root) { animation: pixelate-out 1.4s ease forwards;}
html:active-view-transition-type(pixelate-dissolve)::view-transition-new(root) { animation: pixelate-in 1.4s ease forwards;}
@keyframes pixelate-out { 0% { filter: blur(0px); opacity: 1; } 100% { filter: blur(40px); opacity: 0; }}
@keyframes pixelate-in { 0% { filter: blur(40px); opacity: 0; } 100% { filter: blur(0px); opacity: 1; }}Wipe up
Essa receita usa a propriedade clip-path para produzir um wipe para cima: o conteúdo da nova página desliza de baixo para cima, substituindo o conteúdo “antigo”.
A mecânica é a seguinte: a página de saída parte do valor padrão inset(0 0 0 0) e tem o parâmetro inferior levado a 100%, criando um movimento de cima para baixo. A nova página começa recortada a partir do topo em 100% e desce até 0.
@media (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; types: wipe-up; }}
html:active-view-transition-type(wipe-up)::view-transition-old(root) { animation: wipe-out 1.4s ease forwards;}
html:active-view-transition-type(wipe-up)::view-transition-new(root) { animation: wipe-in 1.4s ease forwards;}
@keyframes wipe-out { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(0 0 100% 0); }}
@keyframes wipe-in { from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0 0 0 0); }}Dá para variar o efeito ajustando os parâmetros do inset(). Para um wipe da esquerda para a direita:
@keyframes wipe-out { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(0 0 0 100%); }}
@keyframes wipe-in { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); }}E, para um wipe de cima para baixo:
@keyframes wipe-out { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(100% 0 0 0); }}
@keyframes wipe-in { from { clip-path: inset(0 0 100% 0); } to { clip-path: inset(0 0 0 0); }}Rotate in-out
Essa próxima é um pouco, digamos, estranha. Definitivamente não é a coisa mais prática do mundo, mas serve para mostrar o quanto view transitions podem ser estendidas.
O efeito combina as funções scale() e rotate(): a página de saída é reduzida a zero e rotacionada em 180 graus no sentido horário, enquanto a nova página entra escalando de zero e girando -180 graus (sentido anti-horário).
Ajustes de opacidade reforçam a ilusão de saída e chegada do conteúdo.
@media (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; types: zoom-rotate; }}
html:active-view-transition-type(zoom-rotate)::view-transition-old(root) { animation: zoom-rotate-out 1.4s ease forwards; transform-origin: center;}
html:active-view-transition-type(zoom-rotate)::view-transition-new(root) { animation: zoom-rotate-in 1.4s ease forwards; transform-origin: center;}
@keyframes zoom-rotate-out { to { transform: scale(0) rotate(180deg); opacity: 0; }}
@keyframes zoom-rotate-in { from { transform: scale(0) rotate(-180deg); opacity: 0; }}Circle wipe-out
Esse efeito é mais sutil que o anterior. Ele aparece melhor quando se transiciona entre conteúdos visualmente distintos e fica mais harmônico quando as duas páginas compartilham cores de fundo parecidas.
Uma forma circular parte do centro usando a função circle() dentro de clip-path, expandindo de 0% (invisível) até 150% (ultrapassando as bordas da página), cobrindo toda a viewport.
@media (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; types: circular-wipe; }}
html:active-view-transition-type(circular-wipe)::view-transition-old(root) { animation: circle-wipe-out 1.4s ease forwards;}
html:active-view-transition-type(circular-wipe)::view-transition-new(root) { animation: circle-wipe-in 1.4s ease forwards;}
@keyframes circle-wipe-out { to { clip-path: circle(0% at 50% 50%); }}
@keyframes circle-wipe-in { from { clip-path: circle(0% at 50% 50%); } to { clip-path: circle(150% at 50% 50%); }}Diagonal push
Essa receita empurra a página “antiga” para fora enquanto a “nova” entra em diagonal, do canto inferior direito em direção ao superior esquerdo (ou qualquer outra variação direcional).
Para o movimento saindo pelo canto inferior direito, a animação translada a -100% nos eixos X e Y, empurrando o conteúdo para fora da tela.
A nova página se aproxima do canto oposto, voltando à posição padrão em 0%. A gradação de opacidade suaviza a transição.
@media (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; types: diagonal-push; }}
html:active-view-transition-type(diagonal-push)::view-transition-old(root) { animation: diagonal-out 1.4s ease forwards;}
html:active-view-transition-type(diagonal-push)::view-transition-new(root) { animation: diagonal-in 1.4s ease forwards;}
@keyframes diagonal-out { to { transform: translate(-100%, -100%); opacity: 0; }}
@keyframes diagonal-in { from { transform: translate(100%, 100%); opacity: 0; }}Curtain reveal
Esse efeito simula uma cortina de teatro fechando sobre a página antiga e abrindo para revelar a nova.
A função inset() define retângulos posicionados a 50% das bordas direita e esquerda. Durante a fase de saída, esses insets ficam em 50%; durante a fase de entrada, reduzem para 0, criando o efeito visual de conteúdo emergindo do centro simultaneamente em direção às bordas esquerda e direita.
@media (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; types: curtain; }}
html:active-view-transition-type(curtain)::view-transition-old(root) { animation: curtain-out 1.4s ease forwards;}
html:active-view-transition-type(curtain)::view-transition-new(root) { animation: curtain-in 1.4s ease forwards;}
@keyframes curtain-out { from { clip-path: inset(0 0 0 0); }}
@keyframes curtain-in { from { clip-path: inset(0 50% 0 50%); } to { clip-path: inset(0 0 0 0); }}3D flip
Essa receita simula a animação de uma carta sendo virada: uma página gira para fora ao longo do eixo Z enquanto a página seguinte gira para dentro, fingindo ser o verso e o anverso de uma única carta.
@media (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; types: flip-3d; }}
html:active-view-transition-type(flip-3d)::view-transition-old(root) { animation: flip-out 1.4s ease forwards;}
html:active-view-transition-type(flip-3d)::view-transition-new(root) { animation: flip-in 1.4s ease forwards;}
@keyframes flip-out { 0% { transform: rotateY(0deg) translateX(0vw); } 100% { transform: rotateY(-90deg) translateX(-100vw); opacity: 1; }}
@keyframes flip-in { 0% { transform: rotateY(90deg) translateX(100vw); } 100% { transform: rotateY(0deg) translateX(0vw); }}E aí, alguma receita legal para compartilhar?
A ideia dessas sete receitas é servir de ponto de partida: peças pequenas o suficiente para serem copiadas, testadas e combinadas em efeitos mais complexos. Sempre há espaço para mais exemplos e mais ideias criativas em cima de view transitions.
Para quem quiser explorar mais a fundo, Bramus mantém uma coleção interativa de demos que vale a visita, com implementações que mostram muito bem o quanto ainda dá para inventar com esse recurso.