Blog/Tutorials & Step-by-Step/Animating React with Framer Motion
POST
February 20, 2026
LAST UPDATEDFebruary 20, 2026

Animating React with Framer Motion

A step-by-step guide to adding physics-based animations, layout transitions, and scroll effects to your React applications.

Tags

ReactAnimationFramer Motion
Animating React with Framer Motion
4 min read

Animating React with Framer Motion

TL;DR

Framer Motion provides a declarative, spring-based animation API that makes adding performant animations to React components trivial, from simple mount transitions to complex drag-and-drop interfaces.

Prerequisites

  • React 18+ with TypeScript
  • Basic understanding of CSS transforms and transitions
  • Node.js 18+ installed
  • A React project (Next.js or Vite)

Step 1: Installation and Basic Motion Components

Install Framer Motion and start with the fundamental building block: the motion component.

bash
npm install framer-motion

Every HTML element has a motion counterpart. Replace <div> with <motion.div> to unlock animation capabilities.

tsx
import { motion } from "framer-motion";
 
function FadeInCard() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
      className="card"
    >
      <h2>Welcome</h2>
      <p>This card fades in and slides up on mount.</p>
    </motion.div>
  );
}

The three core props:

  • initial -- the starting state when the component mounts
  • animate -- the target state to animate toward
  • transition -- how the animation behaves (duration, easing, spring physics)

Spring vs Tween Animations

Framer Motion defaults to spring-based animations, which feel more natural than linear or eased tweens because they simulate real-world physics.

tsx
// Spring animation (default) - bouncy and natural
<motion.div
  animate={{ x: 100 }}
  transition={{ type: "spring", stiffness: 300, damping: 20 }}
/>
 
// Tween animation - predictable duration
<motion.div
  animate={{ x: 100 }}
  transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
/>

Use springs for interactive elements (buttons, cards, modals) and tweens for sequences where you need precise timing control.

Step 2: Variants for Orchestrated Animations

Variants let you define named animation states and coordinate animations across parent and child components.

tsx
import { motion } from "framer-motion";
 
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1, // Each child animates 0.1s after the previous
    },
  },
};
 
const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 },
};
 
function StaggeredList({ items }: { items: string[] }) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      {items.map((item) => (
        <motion.li key={item} variants={itemVariants}>
          {item}
        </motion.li>
      ))}
    </motion.ul>
  );
}

When the parent transitions to "visible", each child also transitions to its own "visible" state, but staggered by 0.1 seconds. This creates the cascade effect commonly seen in list animations.

Dynamic Variants

Variants can also be functions that accept custom arguments:

tsx
const itemVariants = {
  hidden: { opacity: 0, x: -20 },
  visible: (index: number) => ({
    opacity: 1,
    x: 0,
    transition: { delay: index * 0.05 },
  }),
};
 
function AnimatedList({ items }: { items: string[] }) {
  return (
    <ul>
      {items.map((item, index) => (
        <motion.li
          key={item}
          custom={index}
          variants={itemVariants}
          initial="hidden"
          animate="visible"
        >
          {item}
        </motion.li>
      ))}
    </ul>
  );
}

Step 3: AnimatePresence for Enter/Exit Animations

React removes elements from the DOM immediately when they unmount. AnimatePresence intercepts this and plays an exit animation first.

tsx
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
 
function Notification({ message, onClose }: { message: string; onClose: () => void }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: -50, scale: 0.95 }}
      animate={{ opacity: 1, y: 0, scale: 1 }}
      exit={{ opacity: 0, y: -20, scale: 0.95 }}
      transition={{ type: "spring", stiffness: 300, damping: 25 }}
      className="notification"
    >
      <p>{message}</p>
      <button onClick={onClose}>Dismiss</button>
    </motion.div>
  );
}
 
function NotificationStack() {
  const [notifications, setNotifications] = useState<{ id: number; message: string }[]>([]);
 
  const addNotification = () => {
    const id = Date.now();
    setNotifications((prev) => [...prev, { id, message: `Notification #${id}` }]);
  };
 
  const removeNotification = (id: number) => {
    setNotifications((prev) => prev.filter((n) => n.id !== id));
  };
 
  return (
    <div>
      <button onClick={addNotification}>Add Notification</button>
      <AnimatePresence>
        {notifications.map((n) => (
          <Notification
            key={n.id}
            message={n.message}
            onClose={() => removeNotification(n.id)}
          />
        ))}
      </AnimatePresence>
    </div>
  );
}

The key prop is critical. AnimatePresence uses it to track which elements are entering, present, or exiting. Always use unique, stable keys.

Mode Control

AnimatePresence accepts a mode prop to control how enter and exit animations interact:

tsx
// "sync" (default) - enter and exit happen simultaneously
<AnimatePresence mode="sync">
 
// "wait" - exit completes before enter starts
<AnimatePresence mode="wait">
 
// "popLayout" - exiting elements are removed from layout flow immediately
<AnimatePresence mode="popLayout">

Use mode="wait" for page transitions where you want the old page to fully exit before the new page enters.

Step 4: Layout Animations

Layout animations automatically animate elements when their position or size changes in the DOM. Add the layout prop and Framer Motion handles the rest.

tsx
import { motion } from "framer-motion";
import { useState } from "react";
 
function ExpandableCard() {
  const [isExpanded, setIsExpanded] = useState(false);
 
  return (
    <motion.div
      layout
      onClick={() => setIsExpanded(!isExpanded)}
      style={{
        width: isExpanded ? 400 : 200,
        height: isExpanded ? 300 : 100,
        borderRadius: 12,
        background: "#3b82f6",
        cursor: "pointer",
      }}
      transition={{ type: "spring", stiffness: 300, damping: 30 }}
    >
      <motion.h3 layout="position">Click to {isExpanded ? "collapse" : "expand"}</motion.h3>
    </motion.div>
  );
}

Shared Layout Animations

Use layoutId to animate an element between two different components or positions. Framer Motion automatically generates a smooth transition between them.

tsx
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
 
interface Tab {
  id: string;
  label: string;
}
 
const tabs: Tab[] = [
  { id: "home", label: "Home" },
  { id: "about", label: "About" },
  { id: "contact", label: "Contact" },
];
 
function AnimatedTabs() {
  const [activeTab, setActiveTab] = useState(tabs[0].id);
 
  return (
    <div className="tab-bar">
      {tabs.map((tab) => (
        <button
          key={tab.id}
          onClick={() => setActiveTab(tab.id)}
          className="tab-button"
        >
          {tab.label}
          {activeTab === tab.id && (
            <motion.div
              layoutId="active-tab-indicator"
              className="tab-indicator"
              transition={{ type: "spring", stiffness: 400, damping: 30 }}
            />
          )}
        </button>
      ))}
    </div>
  );
}

The indicator element slides smoothly between tabs because Framer Motion sees the same layoutId and animates from the old position to the new one.

Step 5: Scroll-Triggered Animations

Use useInView or whileInView to trigger animations when elements enter the viewport.

tsx
import { motion } from "framer-motion";
 
function ScrollRevealSection({ children }: { children: React.ReactNode }) {
  return (
    <motion.section
      initial={{ opacity: 0, y: 60 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: "-100px" }}
      transition={{ duration: 0.6, ease: "easeOut" }}
    >
      {children}
    </motion.section>
  );
}
  • viewport.once: true means the animation only plays once, not every time the element scrolls in and out
  • viewport.margin shrinks the detection area so the animation triggers before the element fully enters the viewport

Scroll-Linked Animations

For animations that progress with the scroll position (parallax, progress bars), use useScroll:

tsx
import { motion, useScroll, useTransform } from "framer-motion";
 
function ParallaxHero() {
  const { scrollYProgress } = useScroll();
 
  const y = useTransform(scrollYProgress, [0, 1], [0, -200]);
  const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
 
  return (
    <motion.div style={{ y, opacity }} className="hero-background">
      <h1>Parallax Hero</h1>
    </motion.div>
  );
}
 
function ReadingProgress() {
  const { scrollYProgress } = useScroll();
 
  return (
    <motion.div
      style={{ scaleX: scrollYProgress, transformOrigin: "left" }}
      className="fixed top-0 left-0 right-0 h-1 bg-blue-500 z-50"
    />
  );
}

useTransform maps one motion value to another. Here, as scrollYProgress goes from 0 to 1, the y position shifts from 0 to -200px, creating a parallax effect.

Step 6: Gesture Animations

Framer Motion provides built-in gesture detection for hover, tap, drag, and focus.

tsx
import { motion } from "framer-motion";
 
function InteractiveButton() {
  return (
    <motion.button
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      whileFocus={{ boxShadow: "0 0 0 3px rgba(59, 130, 246, 0.5)" }}
      transition={{ type: "spring", stiffness: 400, damping: 17 }}
      className="px-6 py-3 bg-blue-500 text-white rounded-lg"
    >
      Click Me
    </motion.button>
  );
}

Drag Gestures

Build draggable elements with constraints and snap-back behavior:

tsx
import { motion } from "framer-motion";
 
function DraggableCard() {
  return (
    <motion.div
      drag
      dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
      dragElastic={0.2}
      dragTransition={{ bounceStiffness: 300, bounceDamping: 20 }}
      whileDrag={{ scale: 1.1, cursor: "grabbing" }}
      className="w-32 h-32 bg-purple-500 rounded-xl cursor-grab"
    />
  );
}
  • dragConstraints limits how far the element can be dragged
  • dragElastic controls how much the element can be pulled beyond constraints (0 = rigid, 1 = fully elastic)
  • dragTransition configures the spring physics when the element snaps back

Putting It All Together

Here is a practical example combining several techniques -- an animated card grid with staggered reveals, hover effects, and a shared layout modal:

tsx
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
 
interface Project {
  id: number;
  title: string;
  description: string;
}
 
const projects: Project[] = [
  { id: 1, title: "Dashboard", description: "Analytics dashboard with real-time charts" },
  { id: 2, title: "E-commerce", description: "Full-stack shopping platform" },
  { id: 3, title: "Chat App", description: "Real-time messaging with WebSocket" },
];
 
function ProjectGrid() {
  const [selectedProject, setSelectedProject] = useState<Project | null>(null);
 
  return (
    <>
      <motion.div
        className="grid grid-cols-3 gap-4"
        initial="hidden"
        animate="visible"
        variants={{
          hidden: {},
          visible: { transition: { staggerChildren: 0.15 } },
        }}
      >
        {projects.map((project) => (
          <motion.div
            key={project.id}
            layoutId={`card-${project.id}`}
            variants={{
              hidden: { opacity: 0, y: 30 },
              visible: { opacity: 1, y: 0 },
            }}
            whileHover={{ y: -4 }}
            onClick={() => setSelectedProject(project)}
            className="p-6 bg-white rounded-xl shadow cursor-pointer"
          >
            <motion.h3 layoutId={`title-${project.id}`}>{project.title}</motion.h3>
          </motion.div>
        ))}
      </motion.div>
 
      <AnimatePresence>
        {selectedProject && (
          <motion.div
            className="fixed inset-0 flex items-center justify-center z-50"
            initial={{ backgroundColor: "rgba(0,0,0,0)" }}
            animate={{ backgroundColor: "rgba(0,0,0,0.5)" }}
            exit={{ backgroundColor: "rgba(0,0,0,0)" }}
            onClick={() => setSelectedProject(null)}
          >
            <motion.div
              layoutId={`card-${selectedProject.id}`}
              className="bg-white rounded-xl p-8 max-w-lg w-full"
              onClick={(e) => e.stopPropagation()}
            >
              <motion.h3 layoutId={`title-${selectedProject.id}`}>
                {selectedProject.title}
              </motion.h3>
              <p>{selectedProject.description}</p>
              <button onClick={() => setSelectedProject(null)}>Close</button>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </>
  );
}

The card smoothly expands from the grid into a centered modal using layoutId, and the title text animates seamlessly between positions.

Next Steps

  • Page transitions -- wrap Next.js route changes with AnimatePresence for smooth page-to-page animations
  • SVG animations -- use motion.path with pathLength to animate line drawings
  • Accessibility -- respect prefers-reduced-motion by wrapping animations with useReducedMotion()
  • Performance profiling -- use React DevTools Profiler to ensure animations do not cause unnecessary re-renders
  • Reusable animation hooks -- abstract common patterns (fade-in, slide-up, scale) into custom hooks

FAQ

What makes Framer Motion better than CSS animations for React?

Framer Motion provides physics-based spring animations, automatic layout transitions, gesture handling, and a declarative API that integrates naturally with React's component model, all without manual keyframe calculations.

Does Framer Motion impact performance?

Framer Motion is highly optimized, using hardware-accelerated transforms and the Web Animations API under the hood. It avoids triggering layout recalculations for most animations, keeping frame rates smooth.

How do you animate component mount and unmount in React?

Use Framer Motion's AnimatePresence component to wrap elements that may be conditionally rendered, combined with initial, animate, and exit props on motion components to define enter and exit animations.

Collaboration

Need help with a project?

Let's Build It

I help startups and established companies design, build, and scale world-class digital products. From deep technical architecture to pixel-perfect UI — let's bring your vision to life.

SH

Article Author

Sadam Hussain

Senior Full Stack Developer

Senior Full Stack Developer with over 7 years of experience building React, Next.js, Node.js, TypeScript, and AI-powered web platforms.

Related Articles

How to Add Observability to a Node.js App with OpenTelemetry
Mar 21, 20265 min read
Node.js
OpenTelemetry
Observability

How to Add Observability to a Node.js App with OpenTelemetry

Learn how to instrument a Node.js app with OpenTelemetry for traces, metrics, and logs, and build a practical observability setup for production debugging.

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js
Mar 21, 20266 min read
Next.js
Node.js
BFF

How to Build a Backend-for-Frontend (BFF) with Next.js and Node.js

A practical guide to building a Backend-for-Frontend with Next.js and Node.js for API aggregation, auth handling, caching, and frontend-specific data shaping.

How I Structure CI/CD for Next.js, Docker, and GitHub Actions
Mar 21, 20265 min read
CI/CD
Next.js
Docker

How I Structure CI/CD for Next.js, Docker, and GitHub Actions

A practical CI/CD blueprint for Next.js apps using Docker and GitHub Actions, including testing, image builds, deployment stages, cache strategy, and release safety.