Nesta postagem falarei sobre uma API muito bacana e que pouca gente está comentando: a View Transitions.
No texto há diversos links, clique neles para ter acesso à implementação no browser.
O que é
A API ViewTransition foi desenvolvida para tornar mais fácil e eficiente a criação de animações e de efeitos visuais.. Um dos benefícios de usá-la é que ela permite transições mais suaves entre páginas e estados de elementos.
No Google Chrome 76, foi implementado o sistema Paint Holding: sistema para corrigir flashes brancos que acontecia quando o usuário mudava de página e o todo o conteúdo era renderizado (desenhado) na tela. No entanto, algumas mudanças ainda ocorriam de forma abrupta e repentina.
Embora a utilização dessa API traga benefícios em termos de animação e transições, é fundamental considerar possíveis problemas de acessibilidade e usabilidade. A composição de dois estados distintos pode causar problemas com elementos aria-wai em leitores de tela.
Criando transições com a nova API
Para iniciar uma transição, basta executar as modificações do DOM dentro da função document.startViewTransition
. No momento que o DOM do HTML for alterado, o navegador criará, no topo da estrutura HTML, uma árvore de pseudoelementos, que será removida após a conclusão da transição.
document.startViewTransition(() => {
document.body.innerHTML = `<your code>`;
});
Árvore de pseudoelementos
Uma vez que o documento captura os estados novo e velho da página ou do elemento a ser modificado, uma estrutura de pseudoelementos como esta é criada:
html
|_::view-transition
├─ ::view-transition-group(name)
│ └─ ::view-transition-image-pair(name)
│ ├─ ::view-transition-old(name)
│ └─ ::view-transition-new(name)
└─ …Other groups…
A função de cada uma deles:
Pseudoelemento | Descrição |
---|---|
::view-transition | Empilha os grupos de cada elemento que sofrerá transição |
::view-transition-group | Agrupa os estados dos elementos que sofrerão a transição |
::view-transition-image-pair | Armazena uma imagem de cada estado: velho e novo |
::view-transition-old | É o elemento que armazena o estado (uma espécie de screenshot) velho do elemento de transição |
::view-transition-new | É o elemento que armazena o estado (uma espécie de screenshot) novo, que será atualizado |
Por padrão, a transição terá o efeito de fade: quando um elemento se esvaece e outro aparece no lugar.
Como as transições funcionam
As etapas de uma transição realizada sem erro ou interrupções são as seguintes:
- A função
document.startViewTransition(callback)
é invocada e retorna a interfaceViewTransition
; - O estado atual da página é capturado;
- A renderização é pausada;
- O callback do passo 1 é executado. Ele é o responsável por realizar as alterações;
- A promise
ViewTransition.updateCallbackbone
é cumprida. Ela pode ser utilizada para você saber quando a alteração no DOM será atualiada; - O novo estado da página, após a mudanças no passo 4, é capturado;
- Os pseudoelementos são criados na raiz da página (dentro do
<html>
); - O estado do passo 2 é aplicado no pseudoelemento;
- A promise
ViewTransition.ready
é cumprida. Ela poderá ser usada para identificar quando a transição estará pronta para iniciar (mais abaixo terá um exemplo com ela) - Ocorre a transição entre os pseudoelementos;
- A promise
ViewTransition.finished
é cumprida.
Criando uma transição simples
Vamos começar com uns exemplos bem simples para facilitar o entendimento, né? 👍
function changePage(data) {
// Altera o conteúdo sem o uso das transições caso o navegador
// não suporte a operação
if (!document.startViewTransition) {
changeContentPage(data);
return;
}
document.startViewTransition(() => changeContentPage(data));
}
Com isso, teremos o seguinte resultado:
Olha só, é super fácil! E dá para melhorar ainda mais. Bora colocar uma animação usando CSS.
::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;
}
@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); }
}
Eis o novo resultado:
Não é tudo. Você tem liberdade para escolher quais elementos sofrerão a transição. 👇
Criando transição com múltiplos elementos
A criação entre dois elementos segue o mesmo padrão que os exemplos anteriores. No entanto, para fazer esta transição, você precisa nomear os elementos com CSS, usando a propriedade: view-transition-name
. Assim, o navegador criará um novo grupo de pseudoelementos que mover-se-ão de uma posição para outra. Quer saber mais? Olha esse exemplo que legal.
Verifique que nomeamos os três elementos que sofreram a transição
#header-menu {
view-transition-name: header-menu;
}
#first {
view-transition-name: el-first;
}
#second {
view-transition-name: el-second;
}
E também alteramos o modo de transição dos elementos #first
e #second
:
::view-transition-old(el-first) {
animation: scale-old 3s both ease-in-out;
}
::view-transition-new(el-first) {
animation: scale-new 3s both ease-in-out;
}
/* --------------------------------------------------- */
::view-transition-old(el-second) {
animation: back-in-up 3s;
}
::view-transition-new(el-second) {
animation: back-in-down 3s;
}
Bem legal, né? Às vezes, precisamos lidar com animações mais complexas, então é legal se preocupar com o debug.
Debugando transições
Nem todos gostam de fazer testes, mas é necessário😁
Nesta seção, aprenderemos como debugar as animações CSS em seu site.
Para isso, basta abrir o DevTools (F12) e escolher a aba "Animations" para ter acesso a animações do seu site. Clicando no botão "pause", você consegue parar a transição, verificar as propriedades, tentar umas novas modificações.
Bem simples. Você também poderá verificar performance e emular outros dispositivos, redução de animação etc.
Criando uma transição entre páginas (SPA - Single Page Application)
Você pode mudar de página ou documento com a mesma facilidade. Mas atenção: esse método é exclusivo para SPA (pelo menos, por enquanto).
Segue um exemplo maroto (funcionará apenas com os nomes Renata e Pâmela).
Esse ficou legal, mas vamos aos comentários de alguns trechos do código e descobrir mais?
Neste evento, toda a navegação do site será interceptada para capturamos a URL atual e a de destino. Será importante para nomearmos os elementos da página inicial.
navigation.addEventListener('navigate', (event) => {
const toUrl = new URL(event.destination.url);
if (location.origin !== toUrl.origin) return;
const fromUrl = new URL(location.pathname, location.href);
event.intercept({
async handler() {
onLinkNavigate(toUrl, fromUrl);
}
})
});
Esse trecho é opcional. Você pode trocá-lo por uma função qualquer e utilizar o evento onclick
nos elementos do tipo Anchor para invocá-la.
Nesta etapa, o conteúdo da página de destino será capturado com a função getPage
(que utiliza a API fetch
para realizar a requisição)
async function onLinkNavigate(toUrl, fromUrl) {
const response = await getPage(toUrl.href);
...
}
Ainda no onLinkNavigate
, temos o código abaixo para capturar o *card* da dançarina escolhida e nomear os elementos html corretamente. A nomeação durante a execução é necessária, pois não é possível ter dois ou mais elementos com o mesmo view-transition-name
.
/**
* @param {{ personImage: HTMLImageElement, personName: HTMLDivElement }} elementsTarget
*/
function defineViewTransitionName(elementsTarget) {
elementsTarget.personImage.classList.add('person-image');
elementsTarget.personName.classList.add('person-name');
}
async function onLinkNavigate(toUrl, fromUrl) {
...
if (toUrl.pathname.startsWith('/dancers')) {
elementsTarget = getTargetElements(toUrl);
if (elementsTarget) defineViewTransitionName(elementsTarget);
}
...
}
Após os elementos serem identificados e nomeados, iniciamos a substituição de todo o conteúdo body
antigo pela nova página.
async function onLinkNavigate(toUrl, fromUrl) {
...
const transition = document.startViewTransition(() => {
document.body.innerHTML = response;
/**
* Se a requisição for de /dancers/* para /index.html
* então nomearemos o card da dançarina
*/
if (fromUrl.pathname.startsWith('/dancers')) {
elementsTarget = getTargetElements(fromUrl);
if (elementsTarget) defineViewTransitionName(elementsTarget);
}
});
transition.finished.then(() => {
if (elementsTarget) {
elementsTarget.personImage.classList.remove('person-image');
elementsTarget.personName.classList.remove('person-name');
}
});
...
}
Após a conclusão da transição, removeremos as classes que definem a propriedade view-transition-name
para evitar duplicidade.
Simples, né? 💁
Mas podemos fazer muito mais.
Criando transições com o javascript 😯
Já acabou, Jéssica?? Nops!!!
Neste último exemplo, veremos como fazer uma animação usando Javascript.
Como vimos anteriormente, a promise transition.ready
será cumprida quando a transição estiver pronta para começar. Nela, adicionamos a animação personalizada. É importante executar a transição nesta promise, pois é o momento que o browser capturou os dois estados necessários (o novo e o velho) e fez a composição.
No exemplo acima, usamos a API nativa Element.animate, porque ela dá o suporte necessário para pseudoelementos.
transition.ready.then(() => {
document.documentElement.animate(
[
{ transform: "rotate(0) scale(1)" },
{ transform: "rotate(360deg) scale(0)" },
],
{
duration: 1500,
easing: "cubic-bezier(0.68,-0.55,0.27,1.55)",
pseudoElement: "::view-transition-old(image)",
}
);
document.documentElement.animate(
{
clipPath: [`circle(0 at 100px 100px)`, `circle(200px at 100px 100px)`],
},
{
duration: 1500,
easing: "ease-in",
pseudoElement: "::view-transition-new(image)",
}
);
});
Trabalhando com frameworks 💁
Caso você trabalhe com algum framework front-end, é possível também utilizar o startViewTransition
. Abaixo segue alguns exemplos com alguns deles.
- Vue JS
- A chave aqui é `nextTick`, que cumpre a promise uma vez que o DOM foi atualizado. Conferir um exemplo.
- Svelte
- Muito semelhante ao Vue, mas o método para aguardar a próxima mudança é `tick`. Conferir um exemplo.
- Lit
- A chave aqui é a promessa `this.updateComplete`, que cumpre a promise uma vez que o DOM foi atualizado. Conferir um exemplo.
- Angular
- Utilize `applicationRef.tick`. Conferir um exemplo.
- React
- Utilize `flushSync`, porém tome cuidado com várias promises retornadas. Conferir um exemplo.
Aplicando interfaces no Typescript 😎
Como é um recurso recente (hoje 2023-08-03), a maioria dos editores não reconhecem a API. Então, caso você utilize o Typescript, basta adicionar o código abaixo em seu global.d.ts
(ou informe o arquivo de tipo em compilerOptions.typeRoots
no tsconfig.json)
export {};
interface ViewTransition {
/**
* A promise that fulfills when the promise returned by updateCallback fulfills,
* or rejects when it rejects.
*/
readonly updateCallbackDone: Promise<undefined>,
/**
* A promise that fulfills once the pseudo-elements for the transition are created,
* and the animation is about to start.
* It rejects if the transition cannot begin. This can be due to misconfiguration,
* such as duplicate 'view-transition-name’s, or if updateCallbackDone returns a
* rejected promise.
* The point that ready fulfills is the ideal opportunity to animate the view
* transition pseudo-elements with the Web Animation API.
*/
readonly ready: Promise<undefined>,
/**
* A promise that fulfills once the end state is fully visible and interactive to
* the user.
* It only rejects if updateCallback returns a rejected promise, as this indicates
* the end state wasn’t created.
* Otherwise, if a transition fails to begin, or is skipped (by skipTransition()),
* the end state is still reached, so finished fulfills.
*/
readonly finished: Promise<undefined>,
/**
* Immediately finish the transition, or prevent it starting.
* This never prevents updateCallback being called, as the DOM change
* is independent of the transition
* If this is called before ready resolves, ready will reject.
* If finished hasn’t resolved, it will fulfill or reject along with
* updateCallbackDone.
*/
skipTransition: () => void,
}
type UpdateCallback = null|Promise<any>;
declare global {
interface Document {
startViewTransition: (updateCallback: UpdateCallback) => ViewTransition;
}
}
Compatibilidade
Referências
CHROMIUM. Blink: renderer/core/view_transition. GitHub, c2021. Disponível em: <https://github.com/chromium/chromium/tree/4817833c534a72d50606e5f97d1e003f5885494d/third_party/blink/renderer/core/view_transition>. Acesso em: 05 ago. 2023.
W3C. CSS View Transitions Module Level 1. W3C, 2012. Disponível em: <https://www.w3.org/TR/css-view-transitions-1/>. Acesso em: 05 ago. 2023.
WICG. View Transitions Explainer. GitHub, 2021. Disponível em: <https://github.com/WICG/view-transitions/blob/main/explainer.md>. Acesso em: 05 ago. 2023.
CAN I USE. View Transitions. Can I Use, [s.d.]. Disponível em: <https://caniuse.com/view-transitions>. Acesso em: 05 ago. 2023.
FIM!! 🎆
Curtiu?? Não curtiu?? Tem alguma dúvida sobre o assunto?
Deixe um comentário para eu saber mais.
Espero que isso tenha sido útil para você. 😊
Top comments (0)