TimoBlog
Back to all posts

Mastering Animations in React with Framer Motion: A Complete Guide

Timothy Benjamin Timothy Benjamin
11 min read
Mastering Animations in React with Framer Motion: A Complete Guide

Table of Contents

Share this post

Mastering Animations in React with Framer Motion

Modern web applications require smooth, engaging animations to create delightful user experiences. Framer Motion has emerged as the go-to animation library for React, offering a declarative API that makes complex animations simple to implement while maintaining excellent performance.

Why Framer Motion?

Framer Motion stands out from other animation libraries for several compelling reasons:

  • Declarative API: Define animations using simple props rather than imperative code
  • Performance: Hardware-accelerated animations that run smoothly at 60fps
  • Gesture Support: Built-in support for drag, hover, tap, and complex gestures
  • Layout Animations: Automatic animations when layout changes occur
  • Server-Side Rendering: Full SSR support for Next.js and other frameworks
  • TypeScript Support: Excellent TypeScript definitions for type safety

Together with React’s component-based architecture, Framer Motion enables developers to create sophisticated animations with minimal code complexity.

Getting Started with Framer Motion

Installation

First, let’s install Framer Motion in your React project:

# Using npm
npm install framer-motion

# Using yarn
yarn add framer-motion

# Using pnpm
pnpm add framer-motion

Basic Setup

Framer Motion works by replacing standard HTML elements with motion components. Here’s a simple example:

import { motion } from 'framer-motion';

function App() {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.5 }}
    >
      <h1>Hello, Framer Motion!</h1>
    </motion.div>
  );
}

Core Animation Concepts

Basic Animations with Initial and Animate

The foundation of Framer Motion animations lies in the initial and animate props:

import { motion } from 'framer-motion';

const FadeInComponent = () => {
  return (
    <motion.div
      initial={{ opacity: 0, y: 50 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.6, ease: "easeOut" }}
      className="p-8 bg-blue-500 text-white rounded-lg"
    >
      <h2>I fade in and slide up!</h2>
    </motion.div>
  );
};

Keyframe Animations

For more complex animations, you can use keyframes:

const BouncingBall = () => {
  return (
    <motion.div
      className="w-16 h-16 bg-red-500 rounded-full"
      animate={{
        y: [0, -100, 0],
        scale: [1, 1.2, 1],
      }}
      transition={{
        duration: 2,
        repeat: Infinity,
        ease: "easeInOut"
      }}
    />
  );
};

Stagger Animations

Create beautiful staggered animations for lists:

import { motion } from 'framer-motion';

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1,
      delayChildren: 0.2
    }
  }
};

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

const StaggeredList = ({ items }) => {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="space-y-4"
    >
      {items.map((item, index) => (
        <motion.li
          key={index}
          variants={itemVariants}
          className="p-4 bg-gray-100 rounded-lg"
        >
          {item}
        </motion.li>
      ))}
    </motion.ul>
  );
};

Advanced Animation Techniques

Gesture-Based Animations

Framer Motion excels at gesture-based interactions:

const InteractiveCard = () => {
  return (
    <motion.div
      className="w-64 h-64 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg cursor-pointer"
      whileHover={{ 
        scale: 1.05,
        boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1)"
      }}
      whileTap={{ scale: 0.95 }}
      drag
      dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
      dragElastic={0.1}
    >
      <div className="p-6 text-white">
        <h3 className="text-xl font-bold">Interactive Card</h3>
        <p>Hover, click, and drag me!</p>
      </div>
    </motion.div>
  );
};

Layout Animations

One of Framer Motion’s most powerful features is automatic layout animations:

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

const ExpandableCard = () => {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <motion.div
      layout
      onClick={() => setIsExpanded(!isExpanded)}
      className="bg-white p-6 rounded-lg shadow-lg cursor-pointer"
      style={{ maxWidth: isExpanded ? '400px' : '200px' }}
    >
      <motion.h3 layout="position" className="text-xl font-bold mb-4">
        Expandable Content
      </motion.h3>
      
      <AnimatePresence>
        {isExpanded && (
          <motion.div
            initial={{ opacity: 0, height: 0 }}
            animate={{ opacity: 1, height: 'auto' }}
            exit={{ opacity: 0, height: 0 }}
            transition={{ duration: 0.3 }}
          >
            <p>This content appears when the card expands!</p>
            <p>Layout animations handle the smooth transition automatically.</p>
          </motion.div>
        )}
      </AnimatePresence>
    </motion.div>
  );
};

Custom Hooks for Reusable Animations

Create custom hooks to reuse animation logic:

import { useAnimation } from 'framer-motion';
import { useInView } from 'framer-motion';
import { useRef, useEffect } from 'react';

// Custom hook for scroll-triggered animations
const useScrollAnimation = () => {
  const controls = useAnimation();
  const ref = useRef(null);
  const inView = useInView(ref, { once: true, threshold: 0.1 });

  useEffect(() => {
    if (inView) {
      controls.start('visible');
    }
  }, [controls, inView]);

  return { ref, controls };
};

// Usage
const ScrollAnimatedComponent = () => {
  const { ref, controls } = useScrollAnimation();

  return (
    <motion.div
      ref={ref}
      initial="hidden"
      animate={controls}
      variants={{
        hidden: { opacity: 0, y: 50 },
        visible: { opacity: 1, y: 0, transition: { duration: 0.6 } }
      }}
      className="p-8 bg-green-500 text-white rounded-lg"
    >
      <h2>I animate when scrolled into view!</h2>
    </motion.div>
  );
};

Building Complex UI Components

Animated Modal

Create a sophisticated modal with backdrop blur and smooth transitions:

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

const modalVariants = {
  hidden: {
    opacity: 0,
    scale: 0.8,
    y: 50
  },
  visible: {
    opacity: 1,
    scale: 1,
    y: 0,
    transition: {
      type: "spring",
      damping: 25,
      stiffness: 300
    }
  },
  exit: {
    opacity: 0,
    scale: 0.8,
    y: 50,
    transition: {
      duration: 0.2
    }
  }
};

const backdropVariants = {
  hidden: { opacity: 0 },
  visible: { opacity: 1 },
  exit: { opacity: 0 }
};

const AnimatedModal = ({ isOpen, onClose, children }) => {
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          className="fixed inset-0 z-50 flex items-center justify-center"
          variants={backdropVariants}
          initial="hidden"
          animate="visible"
          exit="exit"
        >
          {/* Backdrop */}
          <motion.div
            className="absolute inset-0 bg-black bg-opacity-50 backdrop-blur-sm"
            onClick={onClose}
          />
          
          {/* Modal */}
          <motion.div
            className="relative bg-white p-8 rounded-xl shadow-2xl max-w-md w-full mx-4"
            variants={modalVariants}
            onClick={(e) => e.stopPropagation()}
          >
            <button
              onClick={onClose}
              className="absolute top-4 right-4 text-gray-500 hover:text-gray-700"
            >

            </button>
            {children}
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  );
};

Animated Navigation Menu

Build a sliding navigation menu with smooth transitions:

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

const menuVariants = {
  closed: {
    x: '-100%',
    transition: {
      type: 'spring',
      stiffness: 400,
      damping: 40
    }
  },
  open: {
    x: 0,
    transition: {
      type: 'spring',
      stiffness: 400,
      damping: 40
    }
  }
};

const menuItemVariants = {
  closed: { opacity: 0, x: -20 },
  open: { opacity: 1, x: 0 }
};

const AnimatedNavigation = () => {
  const [isOpen, setIsOpen] = useState(false);
  
  const menuItems = [
    'Home', 'About', 'Services', 'Portfolio', 'Contact'
  ];

  return (
    <>
      {/* Menu Button */}
      <button
        onClick={() => setIsOpen(!isOpen)}
        className="fixed top-4 left-4 z-50 p-2 bg-blue-500 text-white rounded-md"
      >
        {isOpen ? '✕' : '☰'}
      </button>

      {/* Overlay */}
      <AnimatePresence>
        {isOpen && (
          <motion.div
            className="fixed inset-0 bg-black bg-opacity-50 z-40"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={() => setIsOpen(false)}
          />
        )}
      </AnimatePresence>

      {/* Menu */}
      <motion.nav
        className="fixed top-0 left-0 h-full w-80 bg-white shadow-2xl z-50"
        variants={menuVariants}
        initial="closed"
        animate={isOpen ? 'open' : 'closed'}
      >
        <div className="p-8 pt-20">
          <motion.ul className="space-y-6">
            {menuItems.map((item, index) => (
              <motion.li
                key={item}
                variants={menuItemVariants}
                transition={{ delay: index * 0.1 }}
              >
                <a
                  href="#"
                  className="block text-xl font-semibold text-gray-800 hover:text-blue-500 transition-colors"
                >
                  {item}
                </a>
              </motion.li>
            ))}
          </motion.ul>
        </div>
      </motion.nav>
    </>
  );
};

Performance Optimization

Transform vs Layout Properties

For optimal performance, prefer transform properties over layout properties:

// ✅ Good - Uses transform (GPU accelerated)
<motion.div
  animate={{ x: 100, scale: 1.2, rotate: 45 }}
/>

// ❌ Avoid - Triggers layout recalculation
<motion.div
  animate={{ left: 100, width: 200, height: 150 }}
/>

Using will-change CSS Property

For complex animations, use the will-change CSS property:

const OptimizedComponent = () => {
  return (
    <motion.div
      style={{ willChange: 'transform' }}
      animate={{ rotate: 360 }}
      transition={{ repeat: Infinity, duration: 2 }}
      className="w-20 h-20 bg-blue-500"
    />
  );
};

Reducing Re-renders

Use variants to prevent unnecessary re-renders:

const cardVariants = {
  idle: { scale: 1 },
  hover: { scale: 1.05 },
  tap: { scale: 0.95 }
};

const OptimizedCard = ({ children }) => {
  return (
    <motion.div
      variants={cardVariants}
      initial="idle"
      whileHover="hover"
      whileTap="tap"
      className="p-6 bg-white rounded-lg shadow-lg"
    >
      {children}
    </motion.div>
  );
};

Integration with React Ecosystem

Using with React Router

Animate route transitions with Framer Motion and React Router:

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

const pageVariants = {
  initial: { opacity: 0, x: '-100vw' },
  in: { opacity: 1, x: 0 },
  out: { opacity: 0, x: '100vw' }
};

const pageTransition = {
  type: 'tween',
  ease: 'anticipate',
  duration: 0.5
};

const AnimatedRoutes = () => {
  const location = useLocation();
  
  return (
    <AnimatePresence mode="wait">
      <Routes location={location} key={location.pathname}>
        <Route
          path="/"
          element={
            <motion.div
              initial="initial"
              animate="in"
              exit="out"
              variants={pageVariants}
              transition={pageTransition}
            >
              <HomePage />
            </motion.div>
          }
        />
        <Route
          path="/about"
          element={
            <motion.div
              initial="initial"
              animate="in"
              exit="out"
              variants={pageVariants}
              transition={pageTransition}
            >
              <AboutPage />
            </motion.div>
          }
        />
      </Routes>
    </AnimatePresence>
  );
};

Integration with State Management

Combine Framer Motion with Redux or Zustand for complex state-driven animations:

import { useSelector } from 'react-redux';
import { motion } from 'framer-motion';

const NotificationBanner = () => {
  const notifications = useSelector(state => state.notifications);
  
  return (
    <AnimatePresence>
      {notifications.map(notification => (
        <motion.div
          key={notification.id}
          initial={{ opacity: 0, y: -50, scale: 0.8 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          exit={{ opacity: 0, y: -50, scale: 0.8 }}
          layout
          className="p-4 mb-2 bg-blue-500 text-white rounded-lg"
        >
          {notification.message}
        </motion.div>
      ))}
    </AnimatePresence>
  );
};

Best Practices and Tips

Animation Accessibility

Always consider accessibility when implementing animations:

import { useReducedMotion } from 'framer-motion';

const AccessibleAnimation = () => {
  const shouldReduceMotion = useReducedMotion();
  
  return (
    <motion.div
      animate={{ x: shouldReduceMotion ? 0 : 100 }}
      transition={{ duration: shouldReduceMotion ? 0 : 0.5 }}
    >
      Respects user motion preferences
    </motion.div>
  );
};

Testing Animated Components

Use testing utilities that work with Framer Motion:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AnimatedButton } from './AnimatedButton';

test('animated button responds to interactions', async () => {
  const user = userEvent.setup();
  render(<AnimatedButton>Click me</AnimatedButton>);
  
  const button = screen.getByRole('button');
  
  // Test hover state
  await user.hover(button);
  expect(button).toHaveStyle('transform: scale(1.05)');
  
  // Test click state
  await user.click(button);
  expect(button).toHaveStyle('transform: scale(0.95)');
});

Conclusion

Framer Motion transforms how we approach animations in React applications, making complex interactions achievable with minimal code. The library’s declarative API, excellent performance, and comprehensive feature set make it an essential tool for modern React development.

Key takeaways for mastering Framer Motion include understanding the core concepts of initial, animate, and variants, leveraging gesture-based interactions for enhanced user experience, utilizing layout animations for seamless UI transitions, optimizing performance by preferring transforms over layout properties, and considering accessibility in all animation implementations.

Whether you’re building simple micro-interactions or complex animated interfaces, Framer Motion provides the tools needed to create engaging, performant animations that delight users. As you continue to explore the library, remember that great animations should feel natural, serve a purpose, and enhance the overall user experience rather than distract from it.

The combination of React’s component architecture and Framer Motion’s animation capabilities opens up endless possibilities for creating memorable web experiences that stand out in today’s competitive digital landscape.

Timothy Benjamin

About Timothy Benjamin

A Freelance Full-Stack Developer who brings company website visions to reality.