Animações no React: Framer Motion e CSS transitions

1. Fundamentos das Animações no Ecossistema React

Animações em React não são apenas um detalhe estético — elas impactam diretamente a percepção de performance e a experiência do usuário. Uma transição suave entre estados pode transformar uma interface confusa em uma experiência fluida e intuitiva.

No React, as animações podem ser abordadas de duas formas principais: imperativa (manipulando diretamente o DOM via JavaScript) e declarativa (descrevendo o estado final desejado). A abordagem declarativa se alinha perfeitamente com o paradigma do React, onde você declara "o que" deve acontecer, não "como".

O Virtual DOM e o processo de reconciliação do React criam um desafio único: quando o estado muda, o React precisa calcular as diferenças e atualizar o DOM real. Animações mal implementadas podem causar re-renders desnecessários, impactando a performance. Por isso, entender como o React gerencia o ciclo de vida dos componentes é essencial para criar animações eficientes.

2. CSS Transitions e Animações Nativas no React

A maneira mais simples de adicionar animações no React é usando CSS transitions com classes dinâmicas:

import { useState } from 'react';

function FadeInBox() {
  const [visible, setVisible] = useState(false);

  return (
    <div>
      <button onClick={() => setVisible(!visible)}>
        {visible ? 'Esconder' : 'Mostrar'}
      </button>
      <div
        className={`box ${visible ? 'box--visible' : 'box--hidden'}`}
      >
        Conteúdo animado
      </div>
    </div>
  );
}
.box {
  opacity: 0;
  transform: translateY(-20px);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.box--visible {
  opacity: 1;
  transform: translateY(0);
}

Outra técnica útil é usar a propriedade key para forçar a remontagem de componentes, criando animações de entrada/saída:

function ListaAnimada({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={item.id} className="item-entrada">
          {item.text}
        </li>
      ))}
    </ul>
  );
}

Limitações do CSS puro: Animações complexas (timelines, sequências, gestos) exigem JavaScript. Além disso, controlar o momento exato de entrada e saída de componentes é difícil com CSS.

3. Introdução ao Framer Motion: Instalação e Conceitos

O Framer Motion é a biblioteca mais popular para animações declarativas em React. Para instalar:

npm install framer-motion

Componentes fundamentais:

import { motion } from 'framer-motion';

function ComponenteBasico() {
  return (
    <motion.div
      initial={{ opacity: 0, scale: 0.5 }}
      animate={{ opacity: 1, scale: 1 }}
      transition={{ duration: 0.5, ease: 'easeOut' }}
    >
      Animação simples
    </motion.div>
  );
}

As propriedades principais são:
- initial: Estado inicial da animação
- animate: Estado final (pode ser reativo a props/state)
- exit: Estado ao remover o componente (requer AnimatePresence)
- transition: Configurações de timing, easing e delay

4. Animações Avançadas com Framer Motion

Variants: Organização de Estados Reutilizáveis

const variants = {
  hidden: { opacity: 0, x: -100 },
  visible: { 
    opacity: 1, 
    x: 0,
    transition: { staggerChildren: 0.1 }
  },
  exit: { opacity: 0, x: 100 }
};

const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 }
};

function ListaAnimada() {
  return (
    <motion.ul
      variants={variants}
      initial="hidden"
      animate="visible"
      exit="exit"
    >
      {[1, 2, 3].map((item) => (
        <motion.li key={item} variants={itemVariants}>
          Item {item}
        </motion.li>
      ))}
    </motion.ul>
  );
}

Gestos Interativos

function CardInterativo() {
  return (
    <motion.div
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      whileFocus={{ boxShadow: '0 0 0 3px blue' }}
      drag="x"
      dragConstraints={{ left: -100, right: 100 }}
    >
      Arraste-me!
    </motion.div>
  );
}

5. Animações de Entrada e Saída (Mount/Unmount)

O AnimatePresence permite animar a remoção de componentes:

import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';

function ModalAnimado() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? 'Fechar' : 'Abrir'} Modal
      </button>

      <AnimatePresence>
        {isOpen && (
          <motion.div
            key="modal"
            initial={{ opacity: 0, scale: 0.8 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.8 }}
            transition={{ duration: 0.3 }}
            style={{
              position: 'fixed',
              top: '50%',
              left: '50%',
              transform: 'translate(-50%, -50%)',
              background: 'white',
              padding: '2rem',
              borderRadius: '8px'
            }}
          >
            <h2>Modal Animado</h2>
            <p>Este modal tem animação de entrada e saída!</p>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

6. Integração com Gerenciamento de Estado e Roteamento

Transições de Rota com React Router

import { Routes, Route, useLocation } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';

function App() {
  const location = useLocation();

  return (
    <AnimatePresence mode="wait">
      <Routes location={location} key={location.pathname}>
        <Route path="/" element={
          <motion.div
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -20 }}
          >
            Home
          </motion.div>
        } />
        <Route path="/about" element={
          <motion.div
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -20 }}
          >
            About
          </motion.div>
        } />
      </Routes>
    </AnimatePresence>
  );
}

7. Performance e Boas Práticas

Para animações otimizadas, siga estas práticas:

import { motion } from 'framer-motion';

// 1. Use transform em vez de propriedades que causam layout
<motion.div
  animate={{ x: 100 }} // Prefira x/y em vez de left/top
  style={{ willChange: 'transform' }}
/>

// 2. Evite re-renders com React.memo
const CardAnimado = React.memo(({ item }) => (
  <motion.div whileHover={{ scale: 1.02 }}>
    {item}
  </motion.div>
));

// 3. Use useMemo para variants complexas
const variants = useMemo(() => ({
  hidden: { opacity: 0 },
  visible: { opacity: 1 }
}), []);

8. Projeto Prático: Componente de Carrossel Animado

import { useState, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';

const slides = [
  { id: 1, color: '#ff6b6b', text: 'Slide 1' },
  { id: 2, color: '#4ecdc4', text: 'Slide 2' },
  { id: 3, color: '#45b7d1', text: 'Slide 3' }
];

const slideVariants = {
  enter: (direction) => ({
    x: direction > 0 ? 300 : -300,
    opacity: 0
  }),
  center: {
    x: 0,
    opacity: 1
  },
  exit: (direction) => ({
    x: direction < 0 ? 300 : -300,
    opacity: 0
  })
};

function CarrosselAnimado() {
  const [[page, direction], setPage] = useState([0, 0]);
  const slideIndex = ((page % slides.length) + slides.length) % slides.length;

  const paginate = useCallback((newDirection) => {
    setPage([page + newDirection, newDirection]);
  }, [page]);

  return (
    <div style={{ width: 300, height: 200, overflow: 'hidden', position: 'relative' }}>
      <AnimatePresence initial={false} custom={direction}>
        <motion.div
          key={page}
          custom={direction}
          variants={slideVariants}
          initial="enter"
          animate="center"
          exit="exit"
          transition={{ x: { type: 'spring', stiffness: 300, damping: 30 }, opacity: { duration: 0.2 } }}
          drag="x"
          dragConstraints={{ left: 0, right: 0 }}
          dragElastic={1}
          onDragEnd={(e, { offset, velocity }) => {
            const swipe = Math.abs(offset.x) * velocity.x;
            if (swipe < -10000) paginate(1);
            else if (swipe > 10000) paginate(-1);
          }}
          style={{
            position: 'absolute',
            width: '100%',
            height: '100%',
            background: slides[slideIndex].color,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            fontSize: '2rem',
            color: 'white',
            cursor: 'grab'
          }}
        >
          {slides[slideIndex].text}
        </motion.div>
      </AnimatePresence>

      <button
        onClick={() => paginate(-1)}
        style={{ position: 'absolute', left: 10, top: '50%' }}
      >
        ‹
      </button>
      <button
        onClick={() => paginate(1)}
        style={{ position: 'absolute', right: 10, top: '50%' }}
      >
        ›
      </button>
    </div>
  );
}

Este carrossel demonstra:
- Animações de entrada/saída com direção
- Gestos de arrastar (drag) para navegação
- Controle de timeline com spring physics
- Integração com estado para controle de navegação

Referências