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



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:

100%
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:

  1. Quando a lista de tarefas é carregada, cada tarefa aparece deslizando da esquerda.
  2. 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.
  3. 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

CSS
/* 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:

▶ Exemplo 1: Implementação de um componente de acordeão usando transições CSS

JSX 📖 Somente leitura
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>
  )
}
57 linhas de lógica (limite de 40, somente leitura)

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

CSS
/* 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

JSX
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
▶ Experimente
CSS
/* 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

BASH
npm install framer-motion

▶ Exemplo 3: Implementação de uma lista de tarefas com o Framer Motion

JSX 📖 Somente leitura
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
84 linhas de lógica (limite de 40, somente leitura)

Este código demonstra quatro recursos principais do Framer Motion:

  1. initial / animate / exit: Definem os estados de entrada, exibição e saída do componente. AnimatePresence permite que a renderização condicional também acione a animação de saída.
  2. layout prop: Quando um item da lista muda de posição devido a uma alteração no estado done, ele faz uma transição suave e automática para a nova posição, sem a necessidade de calcular coordenadas.
  3. transition: Utiliza a animação de mola física spring; stiffness controla a rigidez da mola; damping controla o amortecimento (quanto maior o valor, menor a elasticidade)
  4. 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

JSX 📖 Somente leitura
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>
  )
}
89 linhas de lógica (limite de 40, somente leitura)

Pontos-chave para a criação de animações pop-up:



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

JSX
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>
  )
}
▶ Experimente

❓ 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 AnimatePresence no Framer Motion às vezes não funciona? R: O motivo mais comum é que o key não está configurado corretamente. O AnimatePresence rastreia os elementos filhos por meio do key; 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 do AnimatePresence devem ser componentes motion.

P: Como garantir o desempenho das animações em dispositivos móveis? R: Limite-se a usar apenas as animações transform e opacity, pois essas propriedades são executadas na camada de composição da GPU do dispositivo móvel. O Framer Motion usa transform por padrão, o que garante um bom desempenho. Se for necessário criar animações complexas em dispositivos móveis, você pode usar o hook useWillChange ou definir manualmente will-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 layout pode ser usada em combinação com layoutId e AnimatePresence para obter uma transição de layout estável. Se estiver usando CSS, considere usar transform: translateX() em vez de left para animações de posição.

P: Qual é a diferença entre o useTransition do React 18 e o Framer Motion? R: O useTransition é 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: o useTransition lida com o “atraso na interação”, enquanto o Framer Motion lida com “transições visuais”.


📖 Resumo


📝 Exercícios

  1. 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.
  2. Use a animação CSS para criar um indicador de carregamento: uma animação de círculo giratório. Use @keyframes para controlar a rotação de 0° a 360° e use linear para definir a curva de velocidade, a fim de manter uma velocidade constante.
  3. Use o Framer Motion para criar um botão de ação flutuante (FAB) que possa ser arrastado. A propriedade drag permite que ele seja arrastado para qualquer lugar da tela e, quando o arrastar terminar, use a animação spring para alinhá-lo à borda mais próxima da tela.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%