logo
ia

Transições de página em Next.js: o código, e aquela que apaguei

7 min de leitura
Transições de página em Next.js: o código, e aquela que apaguei

Este artigo faz parte da série "Redesign de site com IA". Episódio anterior: Criar uma landing page com IA.


Porquê animações

Um site não é apenas informação, é uma experiência. E as transições entre páginas são o momento em que a maioria dos sites parte essa experiência. Um flash branco, um recarregamento brusco, e o leitor perde o fio.

Queria que navegar no asuos.ch fosse fluido, quase táctil. Como folhear uma revista em vez de clicar em links.

O que se segue é tanto sobre o que resultou como sobre o que deitei fora. Porque a parte interessante deste episódio é a segunda.


A animação que construí e depois apaguei

A ideia de partida: quando se sai de uma página, ela não desaparece, cai. Como uma folha de papel a deslizar da mesa. Rotação 3D, translação vertical, e um bordo rasgado gerado em SVG para simular a margem irregular de uma folha arrancada.

O Claude Code implementou-a. Um componente com mais de 150 linhas, cerca de 170 linhas de CSS, keyframes, transforms 3D, curvas de aceleração cúbicas. Funcionava, e era espetacular.

Viveu alguns dias. O commit que a substituiu chama-se fix: smooth fade page transitions, header/footer stay static, e retira 211 linhas para acrescentar 49.

Três razões, pela ordem em que me apareceram:

O cabeçalho e o rodapé também caíam. A animação aplicava-se à página inteira. Ora um menu que baloiça em 3D a cada clique não é fluidez, é instabilidade. A correção consistiu em limitar a transição ao conteúdo principal, o que tornava ao mesmo tempo o efeito de queda muito menos legível.

Durava 400 ms. Na primeira visita é encantador. Ao oitavo clique de uma sessão de leitura é um imposto. Uma animação de navegação é um custo pago em cada interação, não uma vez.

Via-se nos Core Web Vitals. Uma rotação 3D num contentor de página inteira repinta muito. Um commit posterior, perf: optimize Core Web Vitals for mobile, continuou a limpeza substituindo um reflow forçado por um duplo requestAnimationFrame.

O que resta hoje cabe em duas transições de opacidade:

// components/PageTransition.tsx, saída
el.style.transition = 'opacity 250ms ease-out'
el.style.opacity = '0'

el.addEventListener(
  'transitionend',
  () => {
    window.scrollTo(0, 0)
    router.push(target)
  },
  { once: true }
)

// Rede de segurança se o transitionend nunca disparar
setTimeout(() => {
  window.scrollTo(0, 0)
  router.push(target)
}, 300)

Menos impressionante de contar. Nitidamente melhor de usar.

A lição, e é a de toda a série: uma IA implementa qualquer ideia de animação em minutos. Isso torna o custo de produção quase nulo, e portanto elimina o filtro natural que antes o fazia desistir das más ideias. O trabalho deslocou-se. Já não está na implementação, está na decisão de guardar ou de deitar fora.


O detalhe que torna o fundido credível

Fazer uma página desaparecer é fácil. Fazê-la reaparecer sem piscar é menos. Se puser a opacidade a zero e depois a um no mesmo ciclo, o browser agrupa as duas alterações e não se vê transição nenhuma.

A solução cabe num duplo requestAnimationFrame:

el.style.transition = 'none'
el.style.opacity = '0'

// Duplo rAF para deixar o browser aplicar opacity:0
// antes de animar, sem provocar um reflow forçado
requestAnimationFrame(() => {
  requestAnimationFrame(() => {
    el.style.transition = 'opacity 350ms ease-in'
    el.style.opacity = '1'
  })
})

A solução que se encontra em todo o lado consiste em ler el.offsetHeight para forçar o browser a recalcular o layout. Funciona, e custa um reflow síncrono a cada navegação. O duplo requestAnimationFrame obtém o mesmo resultado sem esse custo.

Outra escolha a conhecer: a interceção faz-se ao nível do documento, na fase de captura, e não substituindo cada link do site.

document.addEventListener('click', handleClick, true)

O handler ignora os cliques com tecla modificadora, os target="_blank", os downloads, as âncoras, os mailto:, os links externos e os links para a página atual. Esta abordagem tem uma vantagem inesperada que medi cinco meses depois: funciona com qualquer tag <a>, incluindo as que acrescentei muito mais tarde. Quando substituí o meu seletor de língua por links a sério, a navegação client continuou a funcionar sem uma linha a mudar aqui.


A mudança de tema com a View Transitions API

Quando se passa do tema claro ao tema escuro, um círculo propaga-se a partir do botão. E a direção depende do sentido. Para o claro, a onda parte do botão e irradia para fora, como o sol que ilumina. Para o escuro, parte das margens e converge para o botão, como a noite que cai.

Não é feito com uma camada de ecrã inteiro, mas com a API nativa startViewTransition. O componente calcula a origem e o raio necessário para cobrir o ecrã, e depois passa-os ao CSS através de variáveis:

// components/ThemeSwitch.tsx
const x = e.clientX
const y = e.clientY
const maxRadius = Math.hypot(
  Math.max(x, window.innerWidth - x),
  Math.max(y, window.innerHeight - y)
)

document.documentElement.dataset.transition = 'theme'
document.documentElement.style.setProperty('--theme-switch-x', `${x}px`)
document.documentElement.style.setProperty('--theme-switch-y', `${y}px`)
document.documentElement.style.setProperty('--theme-switch-radius', `${maxRadius}px`)

const transition = document.startViewTransition(() => setTheme(nextTheme))

Ao CSS resta apenas animar um clip-path na pseudo-classe de vista:

@keyframes theme-reveal-out {
  from {
    clip-path: circle(var(--theme-switch-radius) at var(--theme-switch-x) var(--theme-switch-y));
  }
  to {
    clip-path: circle(0px at var(--theme-switch-x) var(--theme-switch-y));
  }
}

O Math.hypot sobre a distância ao canto mais afastado é o que garante que o círculo cobre mesmo o ecrã inteiro, esteja o botão onde estiver. Um raio fixo deixa um canto por cobrir assim que a janela muda de proporções.


O scroll infinito

Nas listagens, os artigos carregam progressivamente. Um IntersectionObserver desencadeia o lote seguinte antes de o leitor chegar ao fundo:

const observer = new IntersectionObserver(
  (entries) => {
    if (entries[0].isIntersecting) loadMore()
  },
  { rootMargin: '200px' }
)

O rootMargin: '200px' é o parâmetro que conta. A sentinela dispara 200 píxeis antes de entrar no campo de visão, portanto o conteúdo seguinte já lá está quando se chega. Sem essa margem, vê-se o vazio, depois o carregamento. Com ela, não se vê nada, que é exatamente o objetivo.


A acessibilidade não é uma opção de fim de obra

O suporte de prefers-reduced-motion está presente nos três sítios, e não como uma redução de duração. As animações são desativadas, não atenuadas. Para algumas pessoas, o movimento no ecrã não é uma questão de gosto mas de conforto físico, ou mesmo de náusea.

if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
  el.style.opacity = '1'
  return
}

E do lado do CSS, para a mudança de tema:

@media (prefers-reduced-motion: reduce) {
  [data-transition='theme']::view-transition-old(root),
  [data-transition='theme']::view-transition-new(root) {
    animation: none !important;
  }
}

A mudança de tema verifica também a disponibilidade da API antes de a usar, e muda sem animação quando ela falta. Uma animação que não degrada com elegância não é uma animação cuidada, é um bug à espera do seu browser.


O que fica

A ideia da página que cai como uma folha de papel veio de uma vontade criativa, não da IA. A implementação, o CSS, os transforms 3D, o SVG de bordo rasgado, foi o Claude Code, em minutos. A decisão de deitar tudo fora foi minha, quatro dias depois.

É aí que está a verdadeira repartição de papéis. A IA tornou a execução quase gratuita. O que continua raro é o discernimento para distinguir o que impressiona numa demo daquilo que aguenta ao oitavo clique.

Estas animações que dão carácter a um site são o género de acabamento que se integra num dia durante um Sprint Digital. Da ideia concreta à entrega, sem semanas de idas e vindas.


Episódio seguinte da série: a auditoria SEO completa do site, e as surpresas que revelou.

Toni Dias

Toni Dias

Engenheiro de software e parceiro técnico · AsuOs

Pronto para transformar o seu negócio digital?

Toni Dias apoia-o na sua estratégia digital com soluções à medida.