·

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

Bagas

@bagas.co
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

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.

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
<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

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).

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

Stiffness & Damping Comparison

// 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
Snappy stiffness: 600 · damping: 30
Floaty stiffness: 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.

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
  • 🏠 Dashboard Active
  • 📊 Analytics
  • 🗂️ Projects
  • ⚙️ Settings

5. Gesture Animations

Framer Motion makes interactive animations trivial with dedicated gesture props.

whileHover and whileTap

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

drag

<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.

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.

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

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.

// Reordering items? just add `layout`
<motion.div layout>
  {item.name}
</motion.div>
// 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

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

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

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.

// ✅ 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:

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

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. 🎬

Available for work

Let's build something together.

Open to freelance projects, collaborations, and full-time roles.

Get in touch

Bagas.

Fullstack developer building clean, fast, and meaningful web experiences.

Pages

© 2026 Bagas. All rights reserved.

SvelteKitSvelte 5Tailwind CSSDrizzle ORMTursobits-ui