Criando transições com API ViewTransition
Tags: braziliandevs, css, frontend, javascript
Published: 2023-08-08 12:52:15

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. 

{% youtube RGaKOtodscA %}

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.

```jsx
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](https://github.com/chromium/chromium/blob/4817833c534a72d50606e5f97d1e003f5885494d/third_party/blink/renderer/core/view_transition/view_transition_style_tracker.cc#L1520-L1572) 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_](https://github.com/chromium/chromium/blob/4817833c534a72d50606e5f97d1e003f5885494d/third_party/blink/renderer/core/css/transition_animations.css): 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:

1. A função [`document.startViewTransition(callback)`](https://github.com/chromium/chromium/blob/4817833c534a72d50606e5f97d1e003f5885494d/third_party/blink/renderer/core/view_transition/view_transition_supplement.cc#L85-L104) é invocada e retorna a interface [`ViewTransition`](https://github.com/chromium/chromium/blob/4817833c534a72d50606e5f97d1e003f5885494d/third_party/blink/renderer/core/view_transition/view_transition.idl);
2. O estado atual da página é capturado;
3. A renderização é pausada;
4. O _callback_ do passo 1 é executado. Ele é o responsável por realizar as alterações;
5. A _promise_ [`ViewTransition.updateCallbackbone`](https://github.com/chromium/chromium/blob/4817833c534a72d50606e5f97d1e003f5885494d/third_party/blink/renderer/core/view_transition/view_transition.cc#L382-L386) é cumprida. Ela pode ser utilizada para você saber quando a alteração no DOM será atualiada;
6. O novo estado da página, após a mudanças no passo 4, é capturado;
7. Os pseudoelementos são criados na raiz da página (dentro do `<html>`);
8. O estado do passo 2 é aplicado no pseudoelemento;
9. A _promise_ [`ViewTransition.ready`](https://github.com/chromium/chromium/blob/4817833c534a72d50606e5f97d1e003f5885494d/third_party/blink/renderer/core/view_transition/view_transition.cc#L376-L380) é cumprida. Ela poderá ser usada para identificar quando a transição estará pronta para iniciar (mais abaixo terá um exemplo com ela)
10. Ocorre a transição entre os pseudoelementos;
11. A _promise_ [`ViewTransition.finished`](https://github.com/chromium/chromium/blob/4817833c534a72d50606e5f97d1e003f5885494d/third_party/blink/renderer/core/view_transition/view_transition.cc#L370-L374) é cumprida.

---

## **Criando uma transição simples**

Vamos começar com uns exemplos bem simples para facilitar o entendimento, né? 👍

```jsx
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:

{% glitch viewtransition---very-simple-transition app %}

Olha só, é super fácil! E dá para melhorar ainda mais. Bora colocar uma animação usando CSS.

```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:

{% glitch viewtransition---simple app %}

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.

{% glitch viewtransition---multiple-elements app %}

Verifique que nomeamos os três elementos que sofreram a transição

```css
#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`:

```css
::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* (<kbd>F12</kbd>) 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.

{% youtube TigPu7sXwXI %}

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_).

{% glitch viewtransition---full app %}

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.

```tsx
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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) 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)

```tsx
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](https://github.com/chromium/chromium/blob/4817833c534a72d50606e5f97d1e003f5885494d/third_party/blink/renderer/core/view_transition/view_transition_style_tracker.cc#L564-L580) com o mesmo `view-transition-name`.

```tsx
/**
 * @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.

```tsx
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.

{% glitch viewtransition---animation-with-javascript app %}

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](https://developer.mozilla.org/en-US/docs/Web/API/Element/animate), porque ela dá o suporte necessário para pseudoelementos.

```tsx
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.

<dl>
  <dt>Vue JS</dt>
  <dd>A chave aqui é `nextTick`, que cumpre a promise uma vez que o DOM foi atualizado. <a href="https://bit.ly/44Wpgwr">Conferir um exemplo</a>.</dd>

  <dt>Svelte</dt>
  <dd>Muito semelhante ao Vue, mas o método para aguardar a próxima mudança é `tick`. <a href="https://svelte.dev/repl/84cffc3241514c1581bf951bdf818def?version=3.55.1">Conferir um exemplo</a>.</dd>

 <dt>Lit</dt>
 <dd>A chave aqui é a promessa `this.updateComplete`, que cumpre a promise uma vez que o DOM foi atualizado. <a href="https://shorturl.at/kCPU7">Conferir um exemplo</a>.</dd>

 <dt>Angular</dt>
 <dd>Utilize `applicationRef.tick`. <a href="https://stackblitz.com/edit/angular-bp4pvy?file=src%2Fmain.ts">Conferir um exemplo</a>.</dd>

 <dt>React</dt>
 <dd>Utilize `flushSync`, porém tome cuidado com várias promises retornadas. <a href="https://codesandbox.io/s/nervous-mclaren-j8v8y0?file=/src/App.tsx">Conferir um exemplo</a>.</dd>
</dl>


---

## **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*)

```tsx
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

![](https://caniuse.bitsofco.de/image/view-transitions.png)

---

## 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](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/](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](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](https://caniuse.com/view-transitions)>. Acesso em: 05 ago. 2023.

---

## FIM!! 🎆


![Homem soltando fogos de artifício porque que você chegou ao fim](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pgux7lz0yqbmovdfkohg.gif)

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ê. 😊