React: Transições e animações
Última atualização: 2026-08-26
Tom tinha acabado de concluir um projeto de painel de dados, mas o gerente de produto observou que as transições entre as páginas eram muito bruscas — os dados apareciam com um “flash” quando a lista era carregada, e as janelas pop-up se abriam e fechavam instantaneamente. Tom percebeu que ter todos os recursos não significa necessariamente uma boa experiência do usuário. Ele precisava adicionar animações de transição suaves às páginas.
1. O que você vai aprender
- Noções básicas e diferenças entre transições e animações em CSS
- O componente de movimento do Framer Motion e o AnimatePresence
- Animações de montagem/desmontagem
- Animação em lote de itens da lista (animação de layout)
- Animações interativas baseadas em gestos (arrastar, passar o mouse)
2. Diagramas conceituais
O diagrama a seguir ajudará você a tomar decisões rápidas na hora de escolher uma opção de animação:
flowchart TD
A[Animation effects are needed] --> B{Effect Complexity}
B -->|Simple Transition| C[CSS Transition]
B -->|Keyframe Animation| D[CSS Animation]
B -->|Complex Interactions| E[Framer Motion]
C --> C1["hover / focus Effect"]
C --> C2["opacity / transform Fade Animation"]
C --> C3["Changes to a Single Attribute"]
D --> D1["@keyframes Defining Multi-Stage"]
D --> D2["Infinite Loop Animation"]
D --> D3["Loading the skeleton screen"]
E --> E1["mount/unmount Animation"]
E --> E2["Drag / Gestures"]
E --> E3["List layout Animation"]
E --> E4["Spring Physical Animation Effects"]
style C fill:#e3f2fd,stroke:#1565c0
style D fill:#e8f5e9,stroke:#2e7d32
style E fill:#fff3e0,stroke:#e65100
3. Um cenário da vida real
O painel do Tom inclui três interações principais: carregamento da lista de tarefas, ativação e desativação da janela modal e arrastar e classificar cartões. Seu objetivo:
- Quando a lista de tarefas é carregada, cada tarefa aparece deslizando da esquerda.
- Quando a janela pop-up é aberta, o fundo aparece gradualmente e a janela pop-up amplia; quando ela é fechada, a animação ocorre na ordem inversa.
- O cartão acompanha o movimento do seu dedo quando você o arrasta e volta ao lugar quando você o solta.
Ele começou com as transições CSS mais simples e, aos poucos, passou a criar animações complexas usando o Framer Motion.
(1) Transições CSS — O aprimoramento progressivo mais simples
As transições CSS são usadas para criar transições suaves entre o estado inicial e o estado final. Elas não exigem nenhuma biblioteca de JavaScript e entram em vigor simplesmente ao serem declaradas no CSS.
As quatro subpropriedades fundamentais da transição
/* transition: property duration timing-function delay */
.example {
transition: opacity 0.3s ease 0s;
/* Complete Syntax:
transition-property: opacity
transition-duration: 0.3s
transition-timing-function: ease
transition-delay: 0s
*/
}
Valores comuns timing-function:
ease: Lento → Rápido → Lento (padrão)ease-in: Lenta → Velocidade constanteease-out: Velocidade constante → Lentaease-in-out: Lento nas duas extremidades, rápido no meiolinear: Velocidade constante durante todo o percursocubic-bezier(n, n, n, n): Curvas de Bézier personalizadas
▶ Exemplo 1: Implementação de um componente de acordeão usando transições CSS
import { useState } from 'react'
const accordionData = [
{ title: 'What is React?', content: 'React It is a tool for building user interfaces. JavaScript library.' },
{ title: 'What Is a Component??', content: 'A component is React The smallest independent unit of an application,Reusable and combinable。' },
{ title: 'What is State?', content: 'State It is variable data within the component,Driver UI Update。' },
]
function Accordion() {
const [openIndex, setOpenIndex] = useState(null)
function toggle(index) {
setOpenIndex(openIndex === index ? null : index)
}
return (
<div style={{ maxWidth: 500, fontFamily: 'sans-serif' }}>
{accordionData.map((item, index) => (
<div
key={index}
style={{
border: '1px solid #e8e8e8',
borderRadius: 8,
marginBottom: 8,
overflow: 'hidden',
}}
>
<button
onClick={() => toggle(index)}
style={{
width: '100%',
padding: '12px 16px',
background: openIndex === index ? '#e6f7ff' : '#fafafa',
border: 'none',
cursor: 'pointer',
fontSize: 15,
fontWeight: 600,
textAlign: 'left',
transition: 'background 0.2s ease',
}}
>
{item.title}
</button>
<div
style={{
maxHeight: openIndex === index ? 80 : 0,
padding: openIndex === index ? '12px 16px' : '0 16px',
opacity: openIndex === index ? 1 : 0,
transition: 'all 0.3s ease',
color: '#666',
fontSize: 14,
lineHeight: 1.6,
}}
>
{item.content}
</div>
</div>
))}
</div>
)
}
Ponto-chave: maxHeight + opacity + padding fazem a transição simultaneamente para obter o efeito de expansão/colapso da área de conteúdo. Observe que o valor de maxHeight deve ser ligeiramente maior do que a altura real do conteúdo.
(2) Animação CSS — Animação baseada em quadros-chave
Quando uma animação tem mais de duas etapas (além de apenas início → fim) ou precisa ser repetida em loop, a Animação CSS é a melhor opção. Ela utiliza @keyframes para definir sequências de animação com várias etapas.
Atributos essenciais
/* animation: name duration timing-function delay iteration-count direction fill-mode */
.pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
▶ Exemplo 2: Animação de carregamento da tela com esqueleto
import './Skeleton.css'
function Skeleton({ count = 3 }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="skeleton-row">
<div className="skeleton-avatar" />
<div style={{ flex: 1 }}>
<div className="skeleton-line" style={{ width: '60%' }} />
<div className="skeleton-line" style={{ width: '90%', marginTop: 8 }} />
</div>
</div>
))}
</div>
)
}
export default Skeleton
/* Skeleton.css */
.skeleton-row {
display: flex;
gap: 12px;
align-items: center;
}
.skeleton-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
.skeleton-line {
height: 14px;
border-radius: 4px;
background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
Como funcionam as animações da tela em esqueleto: O movimento de background-position simula o efeito visual de um rastro de luz que atravessa a tela durante o carregamento. A propriedade infinite na animação CSS faz com que o rastro de luz seja repetido continuamente até que o conteúdo real termine de carregar e substitua a tela em esqueleto.
(3) Framer Motion — Animações complexas no React
A Framer Motion é a biblioteca de animação mais poderosa do ecossistema React, oferecendo três funcionalidades principais:
| Funcionalidade | Problema resolvido | API correspondente |
|---|---|---|
| Animações de entrada/saída | Animações quando os componentes são montados/desmontados | motion.div + AnimatePresence |
| Animação de layout | Transições suaves quando a ordem da lista muda | Propriedade layout |
| Animações de gestos | Gestos de arrastar, passar o cursor e tocar | drag / whileHover / whileTap |
Instalação
npm install framer-motion
▶ Exemplo 3: Implementação de uma lista de tarefas com o Framer Motion
import { useState } from 'react'
import { motion, AnimatePresence, Reorder } from 'framer-motion'
const initialTodos = [
{ id: 1, text: 'Done React Learning Animation', done: false },
{ id: 2, text: 'Reworking the Dashboard Components', done: false },
{ id: 3, text: 'Code Review PR #142', done: false },
{ id: 4, text: 'Update Project Documentation', done: true },
{ id: 5, text: 'Fix the Responsive Navigation BarBUG', done: false },
]
function TodoList() {
const [todos, setTodos] = useState(initialTodos)
function toggleDone(id) {
setTodos(prev =>
prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))
)
}
function removeTodo(id) {
setTodos(prev => prev.filter(t => t.id !== id))
}
return (
<div style={{ maxWidth: 480, fontFamily: 'sans-serif' }}>
<AnimatePresence>
{todos.map(todo => (
<motion.div
key={todo.id}
layout
initial={{ opacity: 0, x: -60 }}
animate={{
opacity: 1,
x: 0,
background: todo.done ? '#f6ffed' : '#fff',
}}
exit={{ opacity: 0, x: 100, height: 0, marginBottom: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
marginBottom: 8,
borderRadius: 8,
border: '1px solid #f0f0f0',
cursor: 'pointer',
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleDone(todo.id)}
style={{ width: 18, height: 18, cursor: 'pointer' }}
/>
<span
style={{
textDecoration: todo.done ? 'line-through' : 'none',
color: todo.done ? '#999' : '#333',
fontSize: 15,
}}
>
{todo.text}
</span>
</div>
<button
onClick={() => removeTodo(todo.id)}
style={{
border: 'none',
background: 'none',
color: '#ff4d4f',
cursor: 'pointer',
fontSize: 18,
fontWeight: 'bold',
}}
>
x
</button>
</motion.div>
))}
</AnimatePresence>
</div>
)
}
export default TodoList
Este código demonstra quatro recursos principais do Framer Motion:
initial/animate/exit: Definem os estados de entrada, exibição e saída do componente.AnimatePresencepermite que a renderização condicional também acione a animação de saída.layoutprop: Quando um item da lista muda de posição devido a uma alteração no estadodone, ele faz uma transição suave e automática para a nova posição, sem a necessidade de calcular coordenadas.transition: Utiliza a animação de mola físicaspring;stiffnesscontrola a rigidez da mola;dampingcontrola o amortecimento (quanto maior o valor, menor a elasticidade)whileHover/whileTap: Animação interativa por gestos; amplia para 1,02x quando o mouse passa por cima e reduz para 0,98x quando clicado
▶ Exemplo 4: Animação de janela modal com movimento no Framer
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
key="overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.45)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}
onClick={onClose}
>
<motion.div
key="modal"
initial={{ scale: 0.85, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.85, opacity: 0, y: 20 }}
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
style={{
background: '#fff',
padding: 24,
borderRadius: 12,
minWidth: 320,
boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
position: 'relative',
}}
onClick={e => e.stopPropagation()}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}
function App() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button
onClick={() => setIsOpen(true)}
style={{ padding: '8px 20px', fontSize: 15, cursor: 'pointer' }}
>
Open the pop-up window
</button>
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
<h2 style={{ margin: '0 0 12px' }}>Confirm Action</h2>
<p style={{ color: '#666', marginBottom: 20 }}>
Are you sure you want to delete this record??This action cannot be undone.。
</p>
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
<button
onClick={() => setIsOpen(false)}
style={{ padding: '8px 20px', cursor: 'pointer' }}
>
Cancel
</button>
<button
onClick={() => {
alert('Deleted')
setIsOpen(false)
}}
style={{
padding: '8px 20px',
background: '#ff4d4f',
color: '#fff',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
}}
>
Confirm Deletion
</button>
</div>
</Modal>
</div>
)
}
Pontos-chave para a criação de animações pop-up:
- As sobreposições e as janelas modais utilizam, cada uma, um conjunto diferente de animações, mas são gerenciadas de maneira uniforme por meio do
AnimatePresence - Use a animação
springpara janelas pop-up,stiffness: 400para que elas respondam rapidamente edamping: 30para reduzir o efeito de salto - Ao entrar: a janela pop-up entra com uma escala de 0,85x + 20 px abaixo
- Ao sair: zoom reverso + movimento para baixo, espelhando a animação de entrada
4. Melhores práticas para o desempenho de animações
| Atributo | Fase de acionamento | Desempenho | Recomendação |
|---|---|---|---|
transform |
Composto | ✅ Melhor | ⭐⭐⭐ |
opacity |
Composto | ✅ Melhor | ⭐⭐⭐ |
filter |
Pintura | ⚠️ Médio | ⭐⭐ |
box-shadow |
Pintura | ⚠️ Médio | ⭐ |
width/height |
Layout | ❌ Caro | ❌ |
top/left/margin |
Layout | ❌ Caro | ❌ |
| Regra | Descrição |
|---|---|
Usar transform e opacity por padrão |
Essas propriedades acionam a thread do compositor sem causar um Layout/Paint |
Evite animações width / height / top / left |
Essas propriedades provocam um reajuste do layout, o que consome muitos recursos |
Use will-change para abrir o navegador |
will-change: transform, opacity para criar uma camada composta com antecedência |
| Evite animar muitos elementos ao mesmo tempo | Animar mais de 20 elementos simultaneamente pode causar quedas de quadros |
Usando requestAnimationFrame |
Evitando o uso de setTimeout em animações baseadas em JavaScript |
▶ Exemplo 5: Animação com keyframes em CSS — Tela de carregamento com esqueleto
function SkeletonCard() {
return (
<div style={{ border: '1px solid #f0f0f0', borderRadius: 8, padding: 16, overflow: 'hidden' }}>
<div style={{
height: 120, borderRadius: 4, marginBottom: 12,
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
}} />
<div style={{
height: 16, width: '60%', borderRadius: 4, marginBottom: 8,
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
}} />
<div style={{
height: 12, width: '40%', borderRadius: 4,
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
}} />
<style>{`
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`}</style>
</div>
)
}
function LoadingGrid() {
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, maxWidth: 800, margin: '0 auto' }}>
{[1, 2, 3].map(i => <SkeletonCard key={i} />)}
</div>
)
}
❓ Perguntas Frequentes
P: Qual é exatamente a diferença entre transições CSS e animações CSS? R: Uma transição é uma transição em duas etapas “de A para B”, acionada por uma mudança de estado (como passar o mouse) e não suporta repetição em loop. Uma animação é uma animação com quadros-chave em várias etapas que pode ser repetida indefinidamente, reproduzida ao contrário e pausada ou retomada. Use transições para cenários simples e animações para sequências complexas.
P: Por que o
AnimatePresenceno Framer Motion às vezes não funciona? R: O motivo mais comum é que okeynão está configurado corretamente. OAnimatePresencerastreia os elementos filhos por meio dokey; se a chave permanecer inalterada, ele interpreta o elemento como “atualizado” em vez de “removido”, e a animação de saída não será acionada. Além disso, os elementos filhos diretos doAnimatePresencedevem ser componentesmotion.
P: Como garantir o desempenho das animações em dispositivos móveis? R: Limite-se a usar apenas as animações
transformeopacity, pois essas propriedades são executadas na camada de composição da GPU do dispositivo móvel. O Framer Motion usatransformpor padrão, o que garante um bom desempenho. Se for necessário criar animações complexas em dispositivos móveis, você pode usar o hookuseWillChangeou definir manualmentewill-change.
P: O que devo fazer se as animações causarem mudanças no layout? R: Reserve um tamanho fixo para os elementos animados, a fim de evitar que o layout mude durante a animação. No Framer Motion, a propriedade
layoutpode ser usada em combinação comlayoutIdeAnimatePresencepara obter uma transição de layout estável. Se estiver usando CSS, considere usartransform: translateX()em vez deleftpara animações de posição.
P: Qual é a diferença entre o
useTransitiondo React 18 e o Framer Motion? R: OuseTransitioné um recurso de concorrência integrado ao React que marca certas atualizações de estado como “não urgentes” (como a filtragem de resultados de pesquisa), permitindo que atualizações urgentes (como digitar em um campo de entrada) sejam processadas primeiro para evitar travamentos. O Framer Motion é uma biblioteca de animação que lida com efeitos visuais, como movimento, redimensionamento e opacidade para elementos da interface do usuário. Os dois abordam questões diferentes: ouseTransitionlida com o “atraso na interação”, enquanto o Framer Motion lida com “transições visuais”.
📖 Resumo
- As transições CSS são adequadas para transições simples de duas etapas que são acionadas automaticamente quando ocorre uma mudança de estado.
- A animação CSS, quando usada com
@keyframes, pode criar animações complexas, com várias etapas e em loop. - Os componentes
motioneAnimatePresencedo Framer Motion abrangem todos os cenários de animação para componentes React - As três propriedades
initial,animateeexitcontrolam os estados de entrada, exibição e saída, respectivamente. layoutOproppermite transições suaves ao reordenar listas, sem a necessidade de calcular manualmente as coordenadas- Princípios fundamentais do desempenho da animação: sempre priorize
transformeopacity
📝 Exercícios
- Use transições CSS para implementar um botão “Voltar ao topo”: ele aparece gradualmente quando a página é rolada mais de 300px; ao ser clicado, ele rola suavemente até o topo; e desaparece gradualmente e fica oculto assim que chega ao topo.
- Use a animação CSS para criar um indicador de carregamento: uma animação de círculo giratório. Use
@keyframespara controlar a rotação de 0° a 360° e uselinearpara definir a curva de velocidade, a fim de manter uma velocidade constante. - Use o Framer Motion para criar um botão de ação flutuante (FAB) que possa ser arrastado. A propriedade
dragpermite que ele seja arrastado para qualquer lugar da tela e, quando o arrastar terminar, use a animaçãospringpara alinhá-lo à borda mais próxima da tela.