Back to Blog
·2 years ago

Mastering Framer Motion — Smooth Animations in React

A complete guide to Framer Motion — from your first animated div to variants, gestures, scroll effects, and layout animations.

B
Mastering Framer Motion — Smooth Animations in React

Animation can be the difference between a good UI and an unforgettable one. Framer Motion is the go-to animation library for React — it gives you production-ready animations with remarkably little code. This guide walks you from zero to confident, covering every concept you'll actually use on the job.

Hit the Replay button on any example below to re-run the animation.

Installation

bash
npm install framer-motion

No additional configuration needed — it works out of the box with Next.js, Vite, and Create React App.


1. The motion Component

Every HTML element has a motion equivalent. Swap <div> for <motion.div> and you unlock the full animation API.

jsx
import { motion } from "framer-motion";
 
// Before
<div className="box" />
 
// After — now animatable ✨
<motion.div className="box" />

Available for every HTML tag: motion.p, motion.button, motion.img, motion.ul, motion.li, and so on.


2. initial, animate, and transition

These three props are the foundation of every Framer Motion animation.

PropPurpose
initialThe starting state
animateThe target state to animate toward
transitionControls how the animation plays
jsx
<motion.div
  initial={{ opacity: 0, y: 24 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5, ease: "easeOut" }}
>
  I fade in from below ✨
</motion.div>
Preview
Animation Complete!

Faded in using easeOut with a subtle scale effect.


3. Transitions — Control the Feel

Framer Motion supports several transition types, each with a distinct character.

tween — Duration-based

jsx
transition={{ type: "tween", duration: 0.4, ease: "easeInOut" }}

spring — Physics-based (most natural)

The most satisfying feel. Controlled by stiffness (how fast) and damping (how bouncy).

jsx
<motion.button
  initial={{ scale: 0 }}
  animate={{ scale: 1 }}
  transition={{ type: "spring", stiffness: 400, damping: 10 }}
>
  Spring! 🌱
</motion.button>
Preview
Springstiffness: 400
🌊Staggereddelay: 0.12s
🚀Launcheddamping: 10

Stiffness & Damping Comparison

jsx
// Stiff spring — snappy
transition={{ type: "spring", stiffness: 600, damping: 30 }}
 
// Soft spring — floaty
transition={{ type: "spring", stiffness: 80, damping: 12 }}
 
// Bouncy spring — playful
transition={{ type: "spring", stiffness: 300, damping: 8 }}
Preview
Snappystiffness: 600 · damping: 30
Floatystiffness: 80 · damping: 12
Bouncy 🏀stiffness: 300 · damping: 8

4. Variants — Scalable Animation State

Once you animate more than two elements, hardcoding props everywhere becomes messy. Variants let you define named animation states and share them across a component tree.

jsx
const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: { staggerChildren: 0.1 },
  },
};
 
const itemVariants = {
  hidden: { opacity: 0, x: -20 },
  visible: { opacity: 1, x: 0 },
};
 
function AnimatedList({ items }) {
  return (
    <motion.ul variants={containerVariants} initial="hidden" animate="visible">
      {items.map((item) => (
        <motion.li key={item.id} variants={itemVariants}>
          {item.label}
        </motion.li>
      ))}
    </motion.ul>
  );
}

The magic: children inherit initial and animate from the parent automatically. The staggerChildren cascades through every child.

Preview

Navigation

  • 🏠DashboardActive
  • 📊Analytics
  • 🗂️Projects
  • ⚙️Settings

5. Gesture Animations

Framer Motion makes interactive animations trivial with dedicated gesture props.

whileHover and whileTap

jsx
<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
  Hover & click me
</motion.button>
Preview

drag

jsx
<motion.div
  drag="x"
  dragConstraints={{ left: -120, right: 120 }}
  whileDrag={{ scale: 1.1, rotate: 4 }}
>
  Drag me ← →
</motion.div>
Preview
drag anywhere

Drag me!


6. AnimatePresence — Animate on Enter & Exit

React removes elements from the DOM instantly. AnimatePresence intercepts that removal and plays an exit animation first.

jsx
import { motion, AnimatePresence } from "framer-motion";
 
function Notification({ show, message }) {
  return (
    <AnimatePresence>
      {show && (
        <motion.div
          key="notification"
          initial={{ opacity: 0, y: -16, scale: 0.95 }}
          animate={{ opacity: 1, y: 0, scale: 1 }}
          exit={{ opacity: 0, y: -16, scale: 0.95 }}
          transition={{ type: "spring", stiffness: 300, damping: 24 }}
        >
          {message}
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Key rule: Always give the motion element inside AnimatePresence a stable key prop — it's how Framer Motion tracks which element to animate out.


7. Scroll Animations with useScroll and useTransform

Create scroll-driven progress bars, parallax effects, and elements that react to scroll position.

jsx
import { useScroll, useTransform, motion } from "framer-motion";
 
function ParallaxHero() {
  const { scrollY } = useScroll();
 
  // opacity goes 1 → 0 as user scrolls 0 → 300px
  const opacity = useTransform(scrollY, [0, 300], [1, 0]);
 
  // background moves at 40% scroll speed (parallax)
  const y = useTransform(scrollY, [0, 300], [0, 120]);
 
  return (
    <motion.section style={{ opacity, y }}>
      <h1>Scroll to fade me</h1>
    </motion.section>
  );
}

Reading Progress Bar

jsx
function ReadingProgressBar() {
  const { scrollYProgress } = useScroll();
 
  return (
    <motion.div
      className="fixed top-0 left-0 h-1 bg-primary origin-left"
      style={{ scaleX: scrollYProgress }}
    />
  );
}

8. Layout Animations

One of Framer Motion's most impressive features — animate layout changes automatically.

jsx
// Reordering items? just add `layout`
<motion.div layout>
  {item.name}
</motion.div>
jsx
// Shared element transitions between states
<motion.img
  layoutId={`product-image-${product.id}`}
  src={product.image}
/>
Preview
🎨
Design
UI/UX
⚙️
Backend
Node.js
📱
Mobile
React Native

9. Real-World Patterns

Page Transition Wrapper

jsx
export function PageTransition({ children }) {
  return (
    <motion.main
      initial={{ opacity: 0, y: 12 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -12 }}
      transition={{ duration: 0.25, ease: "easeInOut" }}
    >
      {children}
    </motion.main>
  );
}

Staggered Card Grid

jsx
const gridVariants = {
  hidden: {},
  visible: { transition: { staggerChildren: 0.08 } },
};
 
const cardVariants = {
  hidden: { opacity: 0, scale: 0.92, y: 16 },
  visible: {
    opacity: 1,
    scale: 1,
    y: 0,
    transition: { type: "spring", stiffness: 260, damping: 20 },
  },
};
 
function ProjectGrid({ projects }) {
  return (
    <motion.div
      className="grid grid-cols-3 gap-6"
      variants={gridVariants}
      initial="hidden"
      animate="visible"
    >
      {projects.map((project) => (
        <motion.div key={project.id} variants={cardVariants}>
          <h3>{project.name}</h3>
        </motion.div>
      ))}
    </motion.div>
  );
}

Animated Accordion Item

jsx
function AccordionItem({ title, content }) {
  const [isOpen, setIsOpen] = useState(false);
 
  return (
    <div>
      <motion.button
        onClick={() => setIsOpen(!isOpen)}
        whileTap={{ scale: 0.98 }}
      >
        {title}
        <motion.span
          animate={{ rotate: isOpen ? 180 : 0 }}
          transition={{ type: "spring", stiffness: 300, damping: 24 }}
        >

        </motion.span>
      </motion.button>
 
      <AnimatePresence initial={false}>
        {isOpen && (
          <motion.div
            key="content"
            initial={{ height: 0, opacity: 0 }}
            animate={{ height: "auto", opacity: 1 }}
            exit={{ height: 0, opacity: 0 }}
            transition={{ duration: 0.25, ease: "easeInOut" }}
            style={{ overflow: "hidden" }}
          >
            {content}
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

10. Performance Tips

Animate only GPU-accelerated properties. Stick to transform and opacity — these never trigger layout recalculations.

jsx
// ✅ GPU-accelerated — smooth at 60fps
animate={{ x: 100, y: 50, scale: 1.1, rotate: 5, opacity: 0.8 }}
 
// ❌ Triggers layout paint — avoid
animate={{ width: 200, height: 100, top: 50, marginLeft: 20 }}

Respect user accessibility preferences:

jsx
import { useReducedMotion } from "framer-motion";
 
function HeroCard() {
  const shouldReduce = useReducedMotion();
 
  return (
    <motion.div
      initial={shouldReduce ? {} : { opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={shouldReduce ? { duration: 0 } : { type: "spring" }}
    />
  );
}

Quick Reference

jsx
import { motion, AnimatePresence, useScroll, useTransform } from "framer-motion";
 
// All core props at a glance
<motion.div
  // Entry & exit states
  initial={{ opacity: 0, scale: 0.9, x: -20 }}
  animate={{ opacity: 1, scale: 1, x: 0 }}
  exit={{ opacity: 0, scale: 0.9 }}
 
  // Transition
  transition={{ type: "spring", stiffness: 260, damping: 20, delay: 0.1 }}
 
  // Gestures
  whileHover={{ scale: 1.04 }}
  whileTap={{ scale: 0.96 }}
  drag="x"
  dragConstraints={{ left: -100, right: 100 }}
 
  // Variants
  variants={myVariants}
 
  // Layout
  layout
  layoutId="unique-shared-id"
/>

Framer Motion rewards you quickly — a few lines produce animations that would take hundreds of lines of raw CSS. Start with initial and animate, reach for variants when logic repeats, then let AnimatePresence and useScroll handle the polish.

Happy animating. 🎬