Pular para o conteúdo
Zumkai

View Transitions no Next.js 16: as três camadas

Uma é Baseline, outra não roda no Firefox e a terceira é experimental no React. Saber qual você está usando decide o que vai para produção.

  • next.js
  • react
Card com as três camadas de View Transitions e o nível de maturidade de cada uma em agosto de 2026.
Sumário
  1. Camada 1: same-document, e por que ela é a única sem ressalva
  2. Camada 2: cross-document, e o que o Firefox faz com ela
  3. Camada 3: o <ViewTransition> do React
  4. O aviso que muda a decisão
  5. As seis limitações do componente React
  6. As quatro receitas, e o que cada uma comunica
  7. Quatro armadilhas que a doc avisa
  8. O que eu usaria hoje em produção
  9. Perguntas frequentes
  10. Fontes

"View Transitions" virou nome de três coisas diferentes, com três níveis de maturidade. Confundi-las é o que faz alguém colocar em produção uma API que a documentação do React chama de não pronta para produção.

CamadaO que éStatus hoje
document.startViewTransition()Transição dentro da mesma páginaBaseline — Chrome 111+, Safari 18+, Firefox 144+
At-rule @view-transitionTransição entre páginas (MPA)Não é Baseline — o Firefox ignora
Componente React <ViewTransition>Transição orquestrada pelo ReactExperimental — só Canary

O Next.js 16 dá acesso às três, porque o App Router roda a última versão Canary do React. Isso é conveniente e perigoso na mesma medida: você consegue usar o componente sem perceber que ele não está estável.

Camada 1: same-document, e por que ela é a única sem ressalva

É a única das três que passou de Baseline neste ano.

A API é uma função só. Você entrega uma callback que muda o DOM, e o browser captura o antes, aplica a mudança, captura o depois e anima entre os dois:

js
document.startViewTransition(() => {
  // qualquer mudança de DOM aqui
  lista.replaceChildren(...novosItens)
})

O suporte hoje cobre os três motores: Chrome 111+, Safari 18+ e Firefox 144+. Sem flag, sem prefixo.

O controle fino vem por CSS, nos pseudo-elementos que o browser cria durante a transição:

css
::view-transition-old(root) { animation: 0.4s ease-in both sair; }
::view-transition-new(root) { animation: 0.4s ease-in both entrar; }

Para transição de elemento compartilhado (a miniatura que vira a imagem grande), o mecanismo é view-transition-name. Dois elementos em estados diferentes com o mesmo nome, e o browser interpola entre eles.

Esta camada é onde eu colocaria qualquer coisa que precise funcionar em produção hoje.

Camada 2: cross-document, e o que o Firefox faz com ela

Aqui a transição acontece entre páginas, no fluxo tradicional de navegação.

O opt-in é uma at-rule, e ela precisa existir nos dois documentos:

css
@view-transition {
  navigation: auto;
}

O descritor navigation aceita auto ou none. Com auto, o documento passa por transição quando quatro condições são atendidas ao mesmo tempo:

  1. A navegação é same-origin
  2. Não há redirect cross-origin no caminho
  3. O navigationType é traverse, push ou replace
  4. Para push e replace, a navegação foi iniciada pelo usuário — não pela interface do browser

Essa lista explica a maior parte dos "não funcionou". Redirect que passa por outro domínio derruba a transição inteira, e navegação disparada por botão do browser não conta como iniciada pelo usuário.

E há o Firefox. O MDN classifica a at-rule como "Limited availability — not Baseline". O Firefox 144 suporta a API same-document, mas ignora a at-rule @view-transition. Fluxo de múltiplas páginas ali simplesmente não anima: a navegação acontece com o corte seco de sempre.

Isso não quebra nada — degrada. É a diferença entre esta camada e a próxima.

O mesmo raciocínio de degradação vale para as scroll-driven animations do CSS: o que degrada sozinho é seguro, o que esconde conteúdo precisa de fallback.

Camada 3: o <ViewTransition> do React

Esta é a que o Next.js 16 entrega e a que exige mais cuidado.

O componente envolve uma subárvore e anima quando ela entra, sai ou muda:

jsx
import { ViewTransition } from 'react'

<ViewTransition enter="auto" exit="auto" default="none">
  <Video />
</ViewTransition>

O React ativa a animação sozinho, conforme o tipo de mudança:

PropDispara quando
enterO <ViewTransition> é inserido dentro de uma Transition
exitEle é removido dentro de uma Transition
updateHá mutação de DOM ou mudança de layout dentro dele
shareExiste um <ViewTransition> de mesmo name na árvore removida e na inserida
defaultFallback para todos os gatilhos acima

Os valores são "auto", "none", um nome de classe CSS, ou um objeto com lógica condicional — o que permite animação diferente para navegação para frente e para trás.

Transição de elemento compartilhado fica declarativa. Mesmo name, árvores diferentes:

jsx
<ViewTransition name="capa"><div className="miniatura" /></ViewTransition>
// ...em outra tela
<ViewTransition name="capa"><div className="miniatura tela-cheia" /></ViewTransition>

E há a via imperativa, para quem quer Web Animations API em vez de CSS. As callbacks onEnter, onExit, onUpdate e onShare recebem uma instância com os pseudo-elementos .old e .new:

jsx
<ViewTransition
  onEnter={(instance) => {
    const anim = instance.new.animate(
      [{ opacity: 0 }, { opacity: 1 }],
      { duration: 500 }
    )
    return () => anim.cancel()
  }}
/>

O aviso que muda a decisão

A documentação do React é explícita sobre o estágio:

O Next.js 16 usa a última Canary do React no App Router. É por isso que o componente está disponível para você sem instalar nada — e é por isso que o aviso é fácil de não ver. A disponibilidade vem do framework; a estabilidade, não.

O que isso significa na prática: usar o <ViewTransition> hoje é aceitar que a API pode mudar num release menor. Para site de cliente com contrato de manutenção, isso é uma dívida que você está assumindo sem avisar. Para projeto próprio ou experimento, é aceitável.

Vale registrar o outro lado, porque ele é real: a Vercel documenta o componente como padrão suportado, com guia oficial de quatro receitas, e afirma que funciona no App Router sem configuração nenhuma — você não precisa instalar react@canary. Existe até uma skill oficial para ensinar as receitas a um agente:

bash
npx skills add vercel-labs/agent-skills --skill vercel-react-view-transitions

As duas coisas são verdade ao mesmo tempo. O React chama de experimental; o Next.js entrega, documenta e suporta. A decisão de risco é sua, e ela deveria ser explícita no orçamento em vez de implícita no código.

Se a transição precisa existir e não pode quebrar, as camadas 1 e 2 fazem o mesmo trabalho com CSS e a API nativa.

As seis limitações do componente React

Todas documentadas, e cinco delas geram sintoma sem erro.

1. Precisa ser o elemento mais externo. O componente só funciona se estiver antes de qualquer nó do DOM. Envolver um <div> em volta dele quebra as animações de entrada e saída.

2. Um por nome, por vez. Só um <ViewTransition name="X"> pode estar montado simultaneamente.

3. Exige uma Transition. Nada acontece fora de startTransition(), <Suspense> ou useDeferredValue. Mudança de estado comum não ativa a animação. Em Next.js isso é menos restritivo do que parece: navegação de rota já é uma transition, então as animações ativam sozinhas na navegação.

4. Só no DOM. Não funciona em React Native.

5. flushSync pula a animação. Atualização síncrona ignora a transição inteira.

6. Acessibilidade é manual. O React não checa prefers-reduced-motion por você.

A sexta merece atenção. Diferente do CSS, onde você escreve a media query uma vez e ela vale, aqui a checagem é responsabilidade sua:

jsx
const reduzido = useMediaQuery('(prefers-reduced-motion: reduce)')

<ViewTransition default={reduzido ? 'none' : 'auto'}>
  <Conteudo />
</ViewTransition>

Sem isso, você está animando para quem pediu explicitamente ao sistema para não animar.

As quatro receitas, e o que cada uma comunica

O guia oficial organiza por significado, não por técnica. É a melhor decisão editorial que a Vercel tomou nessa documentação, e vale copiar o critério.

PadrãoO que comunica ao usuário
Morph de elemento compartilhado"Mesma coisa, indo mais fundo"
Reveal do Suspense"O dado chegou"
Slide direcional"Avançando / voltando"
Crossfade na mesma rota"Mesmo lugar, conteúdo diferente"

O morph é o mais simples e o mais valioso: mesmo name nos dois lados, e o browser interpola tamanho e posição.

jsx
// na grade
<ViewTransition name={`foto-${foto.id}`}>
  <Image src={foto.src} alt={foto.titulo} />
</ViewTransition>

// na página de detalhe
<ViewTransition name={`foto-${foto.id}`}>
  <Image src={foto.src} alt={foto.titulo} fill />
</ViewTransition>

Nenhuma prop extra é necessária. Mas há uma condição de timing: o morph só toca quando o destino renderiza no mesmo commit da navegação — o que acontece com página prefetchada. Se o destino cair num fallback de Suspense primeiro, o par não se forma e o conteúdo entra com a animação de enter.

O slide direcional é a única receita que depende de API específica do Next.js. A prop transitionTypes no <Link> marca a navegação, e o <ViewTransition> mapeia o tipo para a animação:

jsx
<Link href={`/foto/${foto.id}`} transitionTypes={['nav-forward']}>

<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"
>

O useRouter aceita transitionTypes em push() e replace() também. E o tipo não é automático: você decide quais links são "para frente" e quais são "para trás", conforme a hierarquia do seu app.

Quatro armadilhas que a doc avisa

Todas geram sintoma sem erro.

1. O wrapper no layout nunca dispara. Layouts persistem entre navegações, então enter e exit não acontecem lá. O <ViewTransition> de página precisa estar em cada page.tsx.

2. default="none" sem share mata o morph em silêncio. O default="none" existe para impedir que um <ViewTransition> nomeado anime em toda transição não relacionada. Mas se você adicionar default="none" num par nomeado sem manter o share explícito, o par simplesmente para de fazer morph — sem aviso.

3. Cliques durante a transição se perdem. A camada ::view-transition captura eventos de ponteiro enquanto anima. A correção é uma linha:

css
::view-transition { pointer-events: none; }

Mesmo assim o hit-testing continua pulando os participantes nomeados durante a transição — então mantenha as animações curtas e evite nomear elemento que o usuário clica em sequência rápida.

4. Voltar pelo browser não carrega o tipo. Navegação iniciada pelo botão de voltar ou por gesto de swipe não carrega transition type, então o slide direcional não toca. O morph de elemento compartilhado continua valendo, porque ele depende só do name.

Vale também ancorar o header, senão ele desliza junto e o usuário perde a referência espacial:

css
::view-transition-group(site-header) { animation: none; z-index: 100; }
::view-transition-old(site-header) { display: none; }

O display: none no snapshot antigo evita o flash de dois headers visíveis ao mesmo tempo.

O que eu usaria hoje em produção

A regra que sigo tem uma linha: use a camada mais madura que resolve o problema.

Você querUse
Animar troca de conteúdo na mesma páginaCamada 1 — startViewTransition()
Transição entre páginas num site multipáginaCamada 2 — @view-transition, aceitando que o Firefox não anima
Elemento compartilhado entre duas telas do App RouterCamada 3, se o projeto tolerar API instável
Reveal, parallax, barra de progressoNada disso — scroll-driven animations

E o critério de fallback, que vale para as três: como fica se a transição não acontecer?

Se a resposta é "a navegação fica com corte seco", está tudo bem — é o comportamento que a web teve por trinta anos. Se a resposta é "o conteúdo fica invisível" ou "o elemento fica no lugar errado", o problema não é a transição; é que você construiu o estado final assumindo que a animação sempre roda.

Esse teste é o mesmo que aplico às scroll-driven animations, e ele reprova o mesmo tipo de código: o que parte de um estado escondido.

Se você ainda vai atualizar, o que quebra na migração para o Next.js 16 cobre as outras vinte mudanças que vêm junto.

Perguntas frequentes

Posso usar View Transitions em produção hoje?

A camada same-document, sim, sem ressalva — é Baseline nos três motores. A cross-document, sim, aceitando que o Firefox não anima e degrada para corte seco. O componente do React, só se o projeto tolerar uma API que a própria documentação chama de não pronta para produção.

Por que a transição entre páginas não funciona no meu site?

Cheque as quatro condições da camada 2 na ordem: same-origin, sem redirect cross-origin, navigationType compatível, e navegação iniciada pelo usuário. Redirect que passa por outro domínio é a causa mais comum. Depois disso, confirme que a at-rule existe nos dois documentos.

<ViewTransition> funciona sem startTransition?

Não. O componente exige que a atualização aconteça dentro de uma Transition, de um <Suspense> ou de um useDeferredValue. Mudança de estado comum não ativa a animação, e não há erro indicando isso.

Isso substitui o GSAP para transição de página?

Para transição simples entre estados, sim — e sem biblioteca. Para sequência coordenada com várias etapas, controle de easing por trecho e sincronização com scroll, não. A licença e o escopo do GSAP cobrem quando ele continua sendo a ferramenta certa.

Fontes

Verificado em 20 de agosto de 2026.

Gatilho de reavaliação: revisar quando (a) o <ViewTransition> do React sair do canal Canary e estabilizar, (b) o Firefox passar a honrar a at-rule @view-transition, movendo cross-document para Baseline, ou (c) as props do componente mudarem antes da estabilização, o que a própria documentação avisa que pode acontecer.

Leia também

Motion

Motion Design para Web: Guia Completo

Scroll, texto, imagens e vídeo: o catálogo completo de técnicas de motion para web, com implementação em Next.js e os casos em que cada uma compensa.

  • motion
  • scroll