How to Animate Like a Pro Using Framer Motion

Team 8 min read

#framer-motion

#react

#animation

#ux

#tutorial

How to Animate Like a Pro Using Framer Motion

Framer Motion makes it simple to build production‑ready animations in React. Beyond quick fades and slides, it gives you a full toolkit for orchestrating complex sequences, shared element transitions, scroll reveals, and micro‑interactions that feel crisp at 60fps.

This guide distills battle‑tested patterns, code snippets, and performance tips so you can animate like a pro—without fighting the browser.

Prerequisites and Setup

  • A React project (Create React App, Next.js, Vite, etc.)
  • Basic familiarity with JSX

Install:

npm install framer-motion
# or
yarn add framer-motion

Optional performance bundle:

import { LazyMotion, domAnimation, m } from "framer-motion";

The Building Blocks

Framer Motion exports pre‑animated elements called motion components (e.g., motion.div). You combine these with animation props to define behavior.

  • initial: starting state
  • animate: target state
  • exit: leave state (used with AnimatePresence)
  • transition: timing, easing, spring physics
  • variants: reusable named states and orchestration

A Minimal Example

import { motion } from "framer-motion";

export function Hero() {
  return (
    <motion.h1
      initial={{ opacity: 0, y: 24 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5, ease: "easeOut" }}
    >
      Animate like a pro
    </motion.h1>
  );
}

Variants and Orchestration (Professional Pattern)

Variants let parents coordinate children: stagger entrances, delay groups, or ensure a timeline.

import { motion } from "framer-motion";

const container = {
  hidden: { opacity: 0 },
  show: {
    opacity: 1,
    transition: {
      when: "beforeChildren",
      staggerChildren: 0.08,
      delayChildren: 0.1,
    },
  },
};

const item = {
  hidden: { opacity: 0, y: 16 },
  show: { opacity: 1, y: 0, transition: { duration: 0.35 } },
};

export function FeatureList() {
  return (
    <motion.ul variants={container} initial="hidden" animate="show">
      {[1, 2, 3].map(i => (
        <motion.li key={i} variants={item}>
          Feature {i}
        </motion.li>
      ))}
    </motion.ul>
  );
}

Keynotes:

  • Use a parent container variant for staggered entrances.
  • when: "beforeChildren" ensures child animations start after the parent begins.

Micro‑Interactions: Hover, Tap, Focus

Use whileHover and whileTap for instant feedback. Add spring transitions for tactile feel.

import { motion } from "framer-motion";

export function PrimaryButton({ children }) {
  return (
    <motion.button
      whileHover={{ scale: 1.04 }}
      whileTap={{ scale: 0.98 }}
      transition={{ type: "spring", stiffness: 450, damping: 30 }}
      className="btn"
    >
      {children}
    </motion.button>
  );
}

Tip: Reinforce with non‑motion cues (color, elevation) for accessibility and reduced‑motion users.

Animate on Scroll: useInView

Reveal content as it enters the viewport without heavy scroll listeners.

import { motion, useInView } from "framer-motion";
import { useRef } from "react";

export function SectionReveal() {
  const ref = useRef(null);
  const inView = useInView(ref, { once: true, margin: "-10% 0px" });

  return (
    <motion.section
      ref={ref}
      initial={{ opacity: 0, y: 32 }}
      animate={inView ? { opacity: 1, y: 0 } : undefined}
      transition={{ duration: 0.5, ease: "easeOut" }}
    >
      <h2>Scroll‑based reveal</h2>
      <p>Smooth entrance when scrolled into view.</p>
    </motion.section>
  );
}

Pro tip: Use once: true to avoid repeated animations during fast scrolls.

Enter/Exit Transitions: AnimatePresence

AnimatePresence ensures exit animations run for components leaving the tree. This is essential for modals, toasts, menus, and route transitions.

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

export function Modal({ open, onClose }) {
  return (
    <AnimatePresence>
      {open && (
        <motion.div
          key="backdrop"
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          onClick={onClose}
          className="backdrop"
        >
          <motion.dialog
            initial={{ opacity: 0, y: 24, scale: 0.98 }}
            animate={{ opacity: 1, y: 0, scale: 1 }}
            exit={{ opacity: 0, y: 12, scale: 0.98 }}
            transition={{ type: "spring", stiffness: 300, damping: 30 }}
            onClick={e => e.stopPropagation()}
          >
            <h3>Modal Title</h3>
            <p>Content goes here.</p>
          </motion.dialog>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Tip: Put a key on animated blocks if you need to force re‑animation on content changes.

Layout and Shared Element Transitions

Framer Motion can animate between layout states automatically.

Auto layout animations

Use the layout prop when elements change size or position.

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

export function ExpandableCard() {
  const [open, setOpen] = useState(false);
  return (
    <motion.article layout className="card" onClick={() => setOpen(o => !o)}>
      <motion.header layout>
        <h4>Expandable Card</h4>
      </motion.header>
      {open && (
        <motion.div layout initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
          <p>More details with smooth size transition.</p>
        </motion.div>
      )}
    </motion.article>
  );
}

Shared element transitions with layoutId

Animate an element seamlessly between two components or routes.

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

export function Gallery() {
  const [active, setActive] = useState(null);
  const items = [{ id: "a" }, { id: "b" }, { id: "c" }];

  return (
    <LayoutGroup>
      <div className="grid">
        {items.map(item => (
          <motion.img
            key={item.id}
            layoutId={item.id}
            src={`/img/${item.id}.jpg`}
            onClick={() => setActive(item)}
          />
        ))}
      </div>

      <AnimatePresence>
        {active && (
          <motion.div
            className="lightbox"
            onClick={() => setActive(null)}
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          >
            <motion.img layoutId={active.id} src={`/img/${active.id}.jpg`} />
          </motion.div>
        )}
      </AnimatePresence>
    </LayoutGroup>
  );
}

Reordering and Drag

Framer Motion includes primitives for drag gestures and list reordering.

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

export function ReorderableList() {
  const [items, setItems] = useState(["Design", "Develop", "Deploy"]);
  return (
    <Reorder.Group axis="y" values={items} onReorder={setItems}>
      {items.map(item => (
        <Reorder.Item
          key={item}
          value={item}
          as={motion.li}
          whileDrag={{ scale: 1.02 }}
          transition={{ type: "spring", stiffness: 500, damping: 35 }}
        >
          {item}
        </Reorder.Item>
      ))}
    </Reorder.Group>
  );
}

Add dragConstraints or dragMomentum={false} if you need tighter control.

Motion Values, Springs, and Derived Animations

For dynamic physics and synchronized effects, use motion values.

import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";

export function DragToFade() {
  const x = useMotionValue(0);
  const xSpring = useSpring(x, { stiffness: 400, damping: 30 });
  const opacity = useTransform(xSpring, [-120, 0, 120], [0, 1, 0]);

  return (
    <motion.div
      drag="x"
      style={{ x: xSpring, opacity }}
      dragConstraints={{ left: -120, right: 120 }}
      className="pill"
    >
      Drag me
    </motion.div>
  );
}

Performance Techniques

  • Prefer transforms over layout properties:
    • Animate opacity, transform (translate/scale/rotate) instead of top/left/width/height when possible.
  • Use LazyMotion to tree‑shake features:
import { LazyMotion, domAnimation, m } from "framer-motion";

export function App() {
  return (
    <LazyMotion features={domAnimation}>
      <m.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
        Content
      </m.div>
    </LazyMotion>
  );
}
  • Keep shadows, blurs, and large box‑filters static during animations; animate wrapper transforms instead.
  • Batch animations with variants to minimize reflows.
  • For large lists, avoid animating every item simultaneously; stagger or virtualize.

Accessibility and Reduced Motion

Respect user preferences and provide non‑motion affordances.

Global config:

import { MotionConfig } from "framer-motion";

export function Root({ children }) {
  return (
    <MotionConfig reducedMotion="user">
      {children}
    </MotionConfig>
  );
}

Conditional transitions:

import { useReducedMotion } from "framer-motion";

export function FadeIn({ children }) {
  const prefersReduced = useReducedMotion();
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={prefersReduced ? { duration: 0 } : { duration: 0.4 }}
    >
      {children}
    </motion.div>
  );
}

Also:

  • Do not rely on motion alone to signal state changes.
  • Maintain keyboard focus order during animations.
  • Ensure reduced‑motion users still get context via instant state changes.

Common Pitfalls and How to Avoid Them

  • Missing exit states: Components pop out instantly. Wrap with AnimatePresence and define exit.
  • Animating layout props unnecessarily: Causes reflow. Prefer transforms or layout for autos.
  • Choppy scroll reveals: Use useInView with once: true and modest durations.
  • Competing animations: Centralize via variants; avoid multiple animate props fighting on the same element.
  • Route transitions: Keep AnimatePresence at the layout level and set unique key per route.

A Polished Card Example (Putting It Together)

import {
  AnimatePresence,
  LazyMotion,
  domAnimation,
  m,
  useInView,
} from "framer-motion";
import { useRef, useState } from "react";

const card = {
  hidden: { opacity: 0, y: 24 },
  show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: "easeOut" } },
  exit: { opacity: 0, y: 12, transition: { duration: 0.25 } },
};

export function PolishedCard() {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  const inView = useInView(ref, { once: true, margin: "-15% 0px" });

  return (
    <LazyMotion features={domAnimation}>
      <m.article
        ref={ref}
        variants={card}
        initial="hidden"
        animate={inView ? "show" : "hidden"}
        exit="exit"
        className="card"
        onClick={() => setOpen(o => !o)}
        whileHover={{ y: -2 }}
        transition={{ type: "spring", stiffness: 300, damping: 30 }}
      >
        <m.header layout>
          <h3>Pro Card</h3>
          <p>Hover, reveal, expand</p>
        </m.header>

        <AnimatePresence initial={false}>
          {open && (
            <m.div
              key="body"
              initial={{ opacity: 0, height: 0 }}
              animate={{ opacity: 1, height: "auto" }}
              exit={{ opacity: 0, height: 0 }}
              transition={{ duration: 0.3 }}
            >
              <p>Extra details with auto layout and presence.</p>
            </m.div>
          )}
        </AnimatePresence>
      </m.article>
    </LazyMotion>
  );
}

Final Checklist for Pro‑Level Motion

  • Define a motion system: durations, easings, and spring presets.
  • Use variants for orchestration and reusability.
  • Respect reduced motion; provide alternative cues.
  • Prefer transforms; avoid animating costly properties.
  • Use LazyMotion and keep animations purposeful and restrained.
  • Test with keyboard, screen readers, and low‑end devices.

With these patterns and practices, your interfaces will feel fast, intentional, and delightful—without adding complexity to your codebase.