DEV Community

Cover image for Criando transições com API ViewTransition
Valdeir S.
Valdeir S.

Posted on • Updated on • Originally published at valdeir.dev

Criando transições com API ViewTransition

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>`;
});
Enter fullscreen mode Exit fullscreen mode

Á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…

Enter fullscreen mode Exit fullscreen mode

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:

  1. A função document.startViewTransition(callback) é invocada e retorna a interface ViewTransition;
  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 é 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 é 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 é 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));
}
Enter fullscreen mode Exit fullscreen mode

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); }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    })
});
Enter fullscreen mode Exit fullscreen mode

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

    ...
}
Enter fullscreen mode Exit fullscreen mode

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

    ...
}
Enter fullscreen mode Exit fullscreen mode

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

    ...

}
Enter fullscreen mode Exit fullscreen mode

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)",
      }
    );
  });
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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!! 🎆

Homem soltando fogos de artifício porque que você chegou ao 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)