Como usar o View Transitions API para animações entre páginas no Next.js
1. Fundamentos da View Transitions API e sua integração com Next.js
A View Transitions API é uma especificação moderna que permite criar animações suaves entre diferentes estados do DOM, seja em navegações entre páginas ou em mudanças dinâmicas de conteúdo. Diferente de bibliotecas JavaScript tradicionais, ela opera no nível do navegador, proporcionando transições nativas e eficientes, sem a necessidade de manipulação manual de estilos ou temporizadores.
No Next.js, a navegação entre rotas ocorre via cliente (client-side navigation) utilizando o App Router. Isso significa que, ao clicar em um link, o Next.js intercepta a requisição, carrega apenas o conteúdo necessário e atualiza a árvore DOM sem recarregar a página inteira. A View Transitions API se encaixa perfeitamente nesse modelo, pois permite animar essa troca de conteúdo de forma declarativa.
Pré-requisitos:
- Next.js versão 14 ou superior com App Router
- Navegador compatível (Chrome 111+, Edge 111+, Opera 97+)
- Conhecimento básico de CSS @keyframes e pseudo-elementos
2. Configuração inicial e ativação da API no Next.js
Para habilitar a View Transitions API no Next.js, precisamos criar um wrapper de navegação personalizado que utilize o hook useRouter e a função document.startViewTransition().
Crie um componente ViewTransitionLink que encapsula o Link do Next.js:
// components/ViewTransitionLink.tsx
'use client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { ReactNode } from 'react';
interface ViewTransitionLinkProps {
href: string;
children: ReactNode;
className?: string;
}
export default function ViewTransitionLink({ href, children, className }: ViewTransitionLinkProps) {
const router = useRouter();
const handleClick = async (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
if (document.startViewTransition) {
const transition = document.startViewTransition(() => {
router.push(href);
});
await transition.finished;
} else {
router.push(href);
}
};
return (
<Link href={href} onClick={handleClick} className={className}>
{children}
</Link>
);
}
Este componente verifica se o navegador suporta a API. Se sim, envolve a navegação em document.startViewTransition(), que captura o estado atual do DOM e anima a transição para o novo estado.
3. Implementando transições básicas entre páginas
Agora vamos implementar uma transição fade-in/fade-out entre páginas estáticas. No arquivo app/layout.tsx, adicione estilos globais:
/* app/globals.css */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
::view-transition-old(root) {
animation: fade-out 0.3s ease-out forwards;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease-in forwards;
}
Agora, utilize o ViewTransitionLink em seus componentes de navegação:
// app/page.tsx
import ViewTransitionLink from '@/components/ViewTransitionLink';
export default function Home() {
return (
<main>
<h1>Página Inicial</h1>
<ViewTransitionLink href="/sobre">
Ir para Sobre
</ViewTransitionLink>
</main>
);
}
// app/sobre/page.tsx
import ViewTransitionLink from '@/components/ViewTransitionLink';
export default function Sobre() {
return (
<main>
<h1>Sobre Nós</h1>
<ViewTransitionLink href="/">
Voltar para Home
</ViewTransitionLink>
</main>
);
}
Ao navegar entre as páginas, você verá uma transição suave de fade.
4. Personalizando animações com pseudo-elementos CSS
A View Transitions API expõe pseudo-elementos como ::view-transition-old e ::view-transition-new que representam, respectivamente, o estado antigo e o novo da página. Podemos criar animações personalizadas com @keyframes.
Vamos implementar uma transição slide horizontal:
/* app/globals.css */
@keyframes slide-out-left {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(-100%);
opacity: 0;
}
}
@keyframes slide-in-right {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
::view-transition-old(root) {
animation: slide-out-left 0.4s ease-in-out forwards;
}
::view-transition-new(root) {
animation: slide-in-right 0.4s ease-in-out forwards;
}
Para transições direcionais (slide para esquerda ou direita dependendo da navegação), podemos modificar o componente ViewTransitionLink para identificar a direção:
// components/ViewTransitionLink.tsx (trecho modificado)
const handleClick = async (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
if (document.startViewTransition) {
const direction = e.currentTarget.dataset.direction || 'forward';
document.documentElement.style.setProperty('--transition-direction', direction);
const transition = document.startViewTransition(() => {
router.push(href);
});
await transition.finished;
} else {
router.push(href);
}
};
E no CSS, use a variável para definir a animação:
/* app/globals.css */
::view-transition-old(root) {
animation: slide-out 0.4s ease-in-out forwards;
animation-name: slide-out-var(--transition-direction, left);
}
5. Gerenciando múltiplos elementos com nomes de view-transition
Para animar elementos específicos que persistem entre rotas, como um logotipo ou cabeçalho, use a propriedade CSS view-transition-name. Isso permite que o navegador rastreie o elemento e anime sua transformação.
No componente do cabeçalho:
// components/Header.tsx
import styles from './Header.module.css';
export default function Header() {
return (
<header className={styles.header}>
<img
src="/logo.png"
alt="Logo"
className={styles.logo}
style={{ viewTransitionName: 'logo' }}
/>
<nav>
{/* Links de navegação */}
</nav>
</header>
);
}
No CSS, anime o logotipo separadamente:
/* components/Header.module.css */
.logo {
view-transition-name: logo;
width: 100px;
height: auto;
}
/* app/globals.css */
::view-transition-old(logo) {
animation: scale-out 0.3s ease-out forwards;
}
::view-transition-new(logo) {
animation: scale-in 0.3s ease-in forwards;
}
@keyframes scale-out {
from { transform: scale(1); }
to { transform: scale(0.8); opacity: 0; }
}
@keyframes scale-in {
from { transform: scale(1.2); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
Isso cria uma animação suave onde o logotipo escala durante a transição, mantendo a continuidade visual.
6. Tratamento de estados de carregamento e fallbacks
Para navegações lentas, combine a View Transitions API com Suspense e estados de loading:
// app/layout.tsx
import { Suspense } from 'react';
import Loading from './loading';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<Suspense fallback={<Loading />}>
{children}
</Suspense>
</body>
</html>
);
}
Para fallback em navegadores sem suporte, use @supports:
/* app/globals.css */
/* Fallback para navegadores sem View Transitions */
@supports not (view-transition-name: root) {
main {
animation: fade-in 0.3s ease-in;
}
}
E no componente ViewTransitionLink, a detecção já está implementada: se a API não estiver disponível, a navegação ocorre normalmente sem animação.
7. Boas práticas e otimização de performance
Para evitar conflitos com animações existentes, evite aplicar view-transition-name a elementos que já possuem animações CSS complexas. Prefira manter as transições da View Transitions API apenas para elementos estruturais principais.
Limitações importantes:
- Elementos com tamanhos variáveis (como conteúdo dinâmico de API) podem causar saltos visuais
- A API não funciona bem com animações CSS concorrentes no mesmo elemento
Para testar performance:
// components/ViewTransitionLink.tsx (trecho de teste)
const handleClick = async (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
if (document.startViewTransition) {
performance.mark('transition-start');
const transition = document.startViewTransition(() => {
router.push(href);
});
await transition.finished;
performance.mark('transition-end');
performance.measure('transition-duration', 'transition-start', 'transition-end');
const measures = performance.getEntriesByName('transition-duration');
console.log(`Transição durou ${measures[0].duration}ms`);
} else {
router.push(href);
}
};
8. Casos de uso avançados e integração com outras APIs
Para páginas de produto com imagens variáveis, atribua view-transition-name dinamicamente:
// app/produto/[id]/page.tsx
export default function ProdutoPage({ params }: { params: { id: string } }) {
const produto = getProduto(params.id);
return (
<div>
<img
src={produto.imagem}
alt={produto.nome}
style={{ viewTransitionName: `produto-${params.id}` }}
/>
<h1>{produto.nome}</h1>
</div>
);
}
Combinando com IntersectionObserver para transições baseadas em scroll:
// components/Galeria.tsx
'use client';
import { useEffect, useRef } from 'react';
export default function Galeria() {
const galeriaRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.viewTransitionName = 'galeria-item';
} else {
entry.target.style.viewTransitionName = 'none';
}
});
}, { threshold: 0.5 });
const items = galeriaRef.current?.querySelectorAll('.galeria-item');
items?.forEach(item => observer.observe(item));
return () => observer.disconnect();
}, []);
return (
<div ref={galeriaRef} className="galeria">
{imagens.map(img => (
<img key={img.id} src={img.url} alt={img.titulo} className="galeria-item" />
))}
</div>
);
}
Isso permite transições suaves ao navegar entre páginas que compartilham elementos de galeria, criando uma experiência visual coesa e profissional.
Referências
- View Transitions API - MDN Web Docs — Documentação oficial completa com exemplos e especificações técnicas da API.
- View Transitions in Next.js - Vercel Blog — Artigo oficial da Vercel sobre implementação de View Transitions no Next.js com App Router.
- Smooth Page Transitions with View Transitions API - CSS-Tricks — Tutorial prático com exemplos de animações personalizadas e pseudo-elementos.
- Next.js View Transitions Example - GitHub — Repositório oficial do Next.js com exemplo funcional de View Transitions.
- View Transitions API: Practical Guide - Chrome Developers — Guia prático do Chrome Developers com dicas de performance e casos de uso avançados.