Framer Motion Animation System

Frontend Engineering intermediate Building with AI ~20 min read

Overview

User interfaces feel alive when they respond to interaction with smooth, intentional motion. A button that scales on press, a modal that fades in, a list that staggers its children into view—these animations communicate state changes, guide attention, and create a sense of direct manipulation. Without a dedicated animation library, achieving this in React means wrestling with CSS transitions, requestAnimationFrame, refs to track DOM elements, and cleanup logic that’s easy to get wrong. The result is brittle animation code scattered across components.

Framer Motion solves this by providing a declarative animation API built specifically for React. Instead of imperatively managing keyframes and timers, you describe the desired state of an element—animate={{ opacity: 1, y: 0 }}—and the library handles interpolation, spring physics, and DOM updates. The motion component wrapper replaces standard HTML elements (motion.div, motion.button) and adds animation props that integrate naturally with React’s component model, including automatic cleanup on unmount.

Production web projects use Framer Motion extensively for page transitions, interactive hover effects on feature cards, staggered list animations in documentation sidebars, and mount/unmount transitions via AnimatePresence. The library’s layout animation system powers smooth reordering in dashboard widgets, while custom spring configurations ensure consistent motion design across the product.

Core Concepts

Motion Components

Every HTML and SVG element has a motion counterpart: motion.div, motion.span, motion.svg, etc. These components accept all standard props plus animation-specific props like animate, initial, exit, transition, whileHover, and whileTap. Under the hood, motion components use a performant animation engine that batches DOM writes and uses hardware-accelerated transforms wherever possible.

Variants: Orchestrating Complex Animations

Variants are named animation states defined as objects. A parent component can propagate variant changes to all children automatically, enabling orchestrated animations like staggered list reveals. The parent defines transition.staggerChildren, and each child animates in sequence without any explicit coordination logic. Variants decouple animation definitions from component rendering, making them reusable and composable.

AnimatePresence for Mount/Unmount

React removes DOM nodes immediately on unmount—there is no built-in way to animate an element out before removal. AnimatePresence wraps conditionally rendered children and delays their removal until the exit animation completes. This is essential for modals, toasts, route transitions, and any UI where elements leave the screen gracefully rather than vanishing.

Layout Animations

When an element’s position or size changes due to a re-render (e.g., items reordering in a list, a panel expanding), adding the layout prop to a motion component makes it automatically animate between its old and new positions using FLIP (First, Last, Invert, Play). This avoids manual measurement and transforms—Framer Motion calculates the difference and animates it with a single prop.

Spring Physics and Custom Easing

Framer Motion defaults to spring-based animations rather than duration-based easing curves. Springs feel more natural because they model physical momentum and settling. You configure springs with stiffness, damping, and mass, or use presets like type: "spring", bounce: 0.3. For precise timing control, tween transitions with cubic-bezier easing are also supported.

How It Works

Basic Motion Component with Animate

The simplest usage replaces a <div> with motion.div and adds initial and animate props. The component automatically transitions between these states on mount.

import { motion } from "framer-motion";

function FadeInCard({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.4, ease: "easeOut" }}
      className="card"
    >
      {children}
    </motion.div>
  );
}

Variants for Staggered Children

Variants define animation states by name. A parent variant with staggerChildren propagates to children automatically, creating staggered reveal effects without manual delay calculations.

import { motion } from "framer-motion";

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

const itemVariants = {
  hidden: { opacity: 0, x: -20 },
  visible: {
    opacity: 1,
    x: 0,
    transition: { type: "spring", stiffness: 300, damping: 24 },
  },
};

function FeatureList({ features }: { features: string[] }) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
    >
      {features.map((feature) => (
        <motion.li key={feature} variants={itemVariants}>
          {feature}
        </motion.li>
      ))}
    </motion.ul>
  );
}

AnimatePresence for Exit Transitions

AnimatePresence wraps conditionally rendered motion components and runs their exit animation before removing them from the DOM. The mode prop controls whether entering and exiting elements animate simultaneously or sequentially.

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

function Modal({ isOpen, onClose, children }: {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
}) {
  return (
    <AnimatePresence mode="wait">
      {isOpen && (
        <motion.div
          key="modal-backdrop"
          className="backdrop"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          onClick={onClose}
        >
          <motion.div
            key="modal-content"
            className="modal"
            initial={{ opacity: 0, scale: 0.9, y: 20 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.95, y: -10 }}
            transition={{ type: "spring", damping: 25, stiffness: 300 }}
            onClick={(e) => e.stopPropagation()}
          >
            {children}
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Layout Animations for Reordering

The layout prop enables automatic FLIP animations when an element’s position changes across re-renders. Combined with AnimatePresence, this handles both reordering and removal of items in a list.

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

interface Task {
  id: string;
  title: string;
  completed: boolean;
}

function TaskList({ tasks }: { tasks: Task[] }) {
  return (
    <AnimatePresence>
      {tasks.map((task) => (
        <motion.div
          key={task.id}
          layout
          initial={{ opacity: 0, height: 0 }}
          animate={{ opacity: 1, height: "auto" }}
          exit={{ opacity: 0, height: 0 }}
          transition={{
            layout: { type: "spring", stiffness: 400, damping: 30 },
            opacity: { duration: 0.2 },
          }}
          className={`task ${task.completed ? "completed" : ""}`}
        >
          {task.title}
        </motion.div>
      ))}
    </AnimatePresence>
  );
}

Gesture-Driven Animations

Framer Motion provides gesture handlers that trigger animation states. whileHover, whileTap, whileFocus, and whileDrag apply animation variants for the duration of the gesture, reverting when the gesture ends.

import { motion } from "framer-motion";

function InteractiveButton({ label, onClick }: {
  label: string;
  onClick: () => void;
}) {
  return (
    <motion.button
      onClick={onClick}
      whileHover={{ scale: 1.05, boxShadow: "0 8px 30px rgba(108,99,255,0.3)" }}
      whileTap={{ scale: 0.97 }}
      transition={{ type: "spring", stiffness: 400, damping: 17 }}
      className="cta-button"
    >
      {label}
    </motion.button>
  );
}

Practical Applications

For instance, a product landing page can use Framer Motion to power the hero section with staggered text reveals, parallax scroll effects on feature sections, and smooth tab transitions in the product showcase.

Each feature card uses whileHover to elevate on mouse-over with a spring animation that overshoots slightly before settling, creating a satisfying tactile feel.

The website project uses AnimatePresence with Next.js App Router to animate route transitions. A layout component wraps page content in a motion component keyed by the route path, fading and sliding pages in and out as the user navigates. The sidebar navigation uses layout animations so that the active indicator bar slides smoothly between items rather than jumping.

Dashboard components in the internal tooling use layout animations for widget reordering. Users drag-and-drop dashboard cards, and the remaining cards animate to fill the gap. The Reorder component from Framer Motion handles the drag detection, reorder logic, and FLIP animation in a single composable API, eliminating the need for a separate drag-and-drop library for simple reordering use cases.

Common Pitfalls

Further Reading