Capítulo 97 de 456

View transitions

Core Idea

O componente <ViewTransition> do React se integra à View Transitions API do browser para animar declarativamente transições entre estados de UI (navegação, loading, troca de conteúdo), sem bibliotecas de animação complexas — funciona out-of-the-box no App Router (React canary).

Key Concepts

  • <ViewTransition> (de react): componente que ativa animação via name/key; disponível sem instalar react@canary manualmente, pois o App Router já usa canary releases.
  • Gatilhos de ativação: apenas Transitions (useTransition), <Suspense> e useDeferredValue ativam <ViewTransition>; setState comum não ativa. Navegações de rota no Next.js já são transitions, então ativam automaticamente.
  • Shared element morphing: dois <ViewTransition> com o mesmo name em páginas diferentes fazem o browser animar automaticamente posição/tamanho entre eles (ex. thumbnail → hero image). Só funciona se o conteúdo de destino renderiza no mesmo commit da navegação (páginas prefetched); se suspende num fallback primeiro, não forma par.
  • share/default="none": share="morph" atribui a classe morph à view transition para customizar via CSS pseudo-elementos; default="none" impede que um <ViewTransition> nomeado anime em toda transição não relacionada da página — sem ele, todo par nomeado anima sempre que qualquer transição roda.
  • enter/exit: props para animar entrada/saída (ex. Suspense fallback saindo com exit="slide-down", conteúdo real entrando com enter="slide-up").
  • transitionTypes (no <Link>/useRouter().push()/.replace()): marca a navegação com um tipo (ex. nav-forward, nav-back) não automático — você decide qual link é "forward" e qual é "back". enter/exit podem ser objetos chaveados por tipo de transição para mapear direção.
  • viewTransitionName em elemento fixo (ex. header): usado com CSS (animation: none, display: none no old snapshot) para ancorar um elemento que não deve se mover durante slides direcionais.
  • ::view-transition { pointer-events: none; }: necessário porque o overlay de transição captura cliques por padrão, perdendo interações durante a animação.
  • Crossfade same-route: <ViewTransition key={slug} share="auto" enter="auto"> — mudar o key faz React tratar conteúdo antigo/novo como par exit/enter (ativando share) em vez de update in-place, ideal para trocar conteúdo dentro da mesma rota (ex. tabs).
  • Wrapper deve ficar em page.tsx, não no layout: layouts persistem entre navegações, então enter/exit nunca disparam ali.
  • Suporte de browser: usa features mais novas da View Transitions API (transition types, view-transition-class), disponíveis em Chromium 125+ e versões recentes de Safari/Firefox; sem suporte, o app funciona normalmente mas sem animação.

Code Examples

import { ViewTransition } from 'react'

function PhotoGrid({ photos }) {
  return photos.map((photo) => (
    <Link key={photo.id} href={`/photo/${photo.id}`}>
      <ViewTransition name={`photo-${photo.id}`}>
        <Image src={photo.src} alt={photo.title} />
      </ViewTransition>
    </Link>
  ))
}
  • O que demonstra: shared element morph — o mesmo name na grid e na página de detalhe faz o browser animar a transição de posição/tamanho automaticamente.
<Suspense fallback={<ViewTransition exit="slide-down" default="none"><PhotoContentSkeleton /></ViewTransition>}>
  <ViewTransition enter="slide-up" default="none">
    <PhotoContent id={id} />
  </ViewTransition>
</Suspense>
  • O que demonstra: Suspense reveal — skeleton sai com slide-down, conteúdo real entra com slide-up.
<ViewTransition
  enter={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
  exit={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
  default="none"
>
  {/* page content */}
</ViewTransition>
  • O que demonstra: mapear tipos de transição (transitionTypes do <Link>) para animações direcionais de entrada/saída.
<ViewTransition key={slug} name="collection-content" share="auto" enter="auto" default="none">
  <CollectionGrid slug={slug} />
</ViewTransition>
  • O que demonstra: crossfade same-route — trocar key pelo slug faz o conteúdo antigo/novo virar par exit/enter em vez de update silencioso.

Reference Tables

PadrãoO que comunica
Shared element (morph)"Mesma coisa, indo mais fundo"
Suspense reveal"Dado carregado"
Directional slide"Indo pra frente / voltando"
Crossfade same-route"Mesmo lugar, conteúdo diferente"

Anti-patterns

  • Usar default="none" sem share explícito num par nomeado: o par silenciosamente para de morphar.
  • Colocar o wrapper de slide direcional no layout em vez do page.tsx: layouts persistem entre navegações, então enter/exit nunca disparam.
  • Ignorar prefers-reduced-motion em slides direcionais: movimento de posição no viewport é o gatilho mais comum de sensibilidade a movimento — zere animation-duration/animation-delay nesse media query.
  • Nomear elementos clicados rapidamente durante uma transição: hit-testing pula participantes nomeados durante a transição (mesmo com pointer-events: none no overlay geral), então mantenha transições curtas.
  • Esperar slide direcional em navegação iniciada pelo browser (botão voltar/gesto): essas navegações não carregam transitionTypes, então o slide direcional não toca (mas o morph de shared element ainda funciona se os name combinarem).

Key Takeaways

  1. View transitions ativam só via Transition/Suspense/useDeferredValue — não via setState comum; navegação de rota no Next.js já conta como transition.
  2. name igual em duas telas = morph automático; key diferente na mesma tela = crossfade (via share="auto").
  3. default="none" é quase sempre necessário em pares nomeados customizados, para não animar em transições não relacionadas.
  4. Direção espacial (esquerda = avançar, direita = voltar) é convenção de motion design que transitionTypes implementa manualmente — não é automático.
  5. Sempre trate pointer-events do overlay e prefers-reduced-motion como parte do checklist de implementação, não como polish opcional.

Connects To

  • Streaming (ch083): Suspense boundaries usadas aqui para os "Suspense reveals" são as mesmas do modelo de streaming.
  • Linking and Navigating (getting-started): transitionTypes é uma prop de <Link>/useRouter, documentada lá.