React Animation Blog
Vol. 01

AnimateyourUIwithFramerMotion

A beginner's guide to installing, importing, and using motion components to bring your React app to life, with live interactive examples throughout.

8Concepts covered
100%Interactive demos
BeginnerDifficulty
framer-motion v11
Step 1 — Install
1
Install Framer Motion

Make sure you have a React project set up — via create-react-app, Vite, or Next.js. Then in your terminal:

npm install framer-motion

Or if you use Yarn:

yarn add framer-motion
Step 2 — Import
2
Importing Motion
import { motion } from "framer-motion";
  • motion is a special component factory from Framer Motion.
  • Wrap any HTML element with motion to animate it — <motion.div>, <motion.h1>, <motion.span>, etc.
  • It behaves exactly like the original element, but now accepts animation props.
Core Motion Props
PropWhat it doesExample
initialStarting state before animation plays{ opacity: 0, y: -40 }
animateTarget state to animate toward{ opacity: 1, y: 0 }
transitionControls how the animation plays{ duration: 0.8 }
exitState when element is removed from DOM{ opacity: 0 }
whileHoverAnimates on mouse hover{ scale: 1.05 }
whileTapAnimates when clicked or tapped{ scale: 0.97 }
Reading the example: initial={{ opacity: 0, y: -40 }} means the element starts invisible and 40px above its natural position. animate={{ opacity: 1, y: 0 }} moves it back down and fades it in. The double curly braces are JSX (outer) + JS object (inner).
Easing — click to preview
easeIn
Starts slow, ends fast. Good for exits.
easeOut
Starts fast, decelerates. Natural entrances.
easeInOut
Slow → fast → slow. Smooth and polished.
linear
Constant speed. Feels mechanical.
backOut
Overshoots then snaps back. Playful.
anticipate
Pullback before moving. Dramatic.
click an ease above, then replay
Transition — tween vs spring

Every Framer Motion animation has a transition prop that controls how it plays. There are two fundamentally different modes — pick the right one and your UI feels alive. Pick the wrong one and it feels scripted.

Tweenduration: 0.8s · ease: easeInOut
Springstiffness: 120 · damping: 14
Notice: the spring overshoots slightly and settles naturally. The tween reaches the end at exactly 0.8s and stops.
Tween keystype: "tween" (default)
transition={{
  duration: 0.8,
  ease: "easeOut",
  delay: 0.2,
}}
KeyWhat it doesExample
durationHow long the animation takes, in seconds.0.8
delayWait before starting. Stagger multiple elements.0.3
easeThe easing curve — string or cubic bezier array."easeOut"
Spring keystype: "spring"
transition={{
  type: "spring",
  stiffness: 120,
  damping: 14,
  mass: 1,
  delay: 0.2,
}}
KeyWhat it doesExample
typeSet to "spring" to switch from tween to physics."spring"
stiffnessHow snappy the spring is. Higher = faster snap.120
dampingResistance to bouncing. Lower = more oscillation.14
massWeight of the element. Higher = slower, heavier feel.1
delayWorks the same as in tweens — wait before starting.0.2
When to use which: Use tween for entrance animations, page transitions, and anything decorative where you want exact timing control. Use spring for anything the user can interact with — hover, tap, drag, and focus states. Springs handle mid-animation interruptions naturally: if the user moves their cursor away before a hover animation finishes, a spring settles from wherever it currently is. A tween either completes or jumps — neither feels right.
Text entrance animation
direction
ease
duration0.8s
<motion.div
  initial={{ opacity: 0, y: -40 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.8, ease: "easeInOut" }}
>
  <h1>Hello, world!</h1>
</motion.div>
live preview

Hello, world!

key={0} — increments on each replay
The key prop trick: Framer Motion entrance animations fire once — on mount. To replay them, increment a key prop on the motion element. React sees a new key, unmounts the old element, mounts a fresh one, and the initial → animate transition fires again from scratch. Without this, the animation runs once and stays frozen. Watch the counter above — it increments every time you hit Replay or change a control.
Animated button — whileHover & whileTap
import { motion } from "framer-motion";

function AnimatedButton({ children, stiffness = 300, damping = 15 }) {
  return (
    <motion.button
      whileHover={{ scale: 1.05, y: -2 }}
      whileTap={{ scale: 0.9, y: 1 }}
      transition={{ type: "spring", stiffness, damping }}
      className="border border-gray-300 px-4 py-2 rounded-md cursor-pointer"
    >
      {children}
    </motion.button>
  );
}
stiffness300
50 = loose and slow → 500 = very snappy
damping15
5 = lots of bounce → 40 = no bounce at all
Prop / gestureWhat it does
whileHoverRuns while the cursor is over the element. Auto-reverses on mouse leave.
whileTapRuns while the element is pressed. Auto-reverses on release.
type: "spring"Physics-based animation. No fixed duration — settles naturally.
stiffnessHow snappy the spring is. Higher = faster snap back.
dampingHow much the spring resists oscillation. Lower = more bounce.
scale: 1.05Grows to 105% of its size on hover.
y: -2 / y: 1Lifts up 2px on hover, presses down 1px on tap.
Why spring for buttons? Tweens (duration + ease) feel pre-recorded. Springs feel physical — they respond to interruptions naturally. If the user moves their cursor away mid-hover, a spring settles from wherever it currently is rather than snapping. This makes interactions feel alive rather than scripted. Try dragging the damping slider to 5 for maximum bounce, then 40 for a stiff, tight snap.
Variants & staggerChildren
import { motion } from "framer-motion";

// Define variants outside the component —
// keeps JSX clean and avoids recreating objects on every render.
const container = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.15,  // each child waits 150ms
      delayChildren: 0.2,     // first child waits 200ms
    },
  },
};

const item = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 },
};

function FeaturesList() {
  const features = ["Fast", "Reliable", "Easy to use", "Customizable", "Secure"];

  return (
    <motion.ul
      variants={container}
      initial="hidden"
      animate="visible"
    >
      {features.map((feature) => (
        // No initial/animate needed on children —
        // the parent propagates its state down automatically.
        <motion.li key={feature} variants={item}>
          {feature}
        </motion.li>
      ))}
    </motion.ul>
  );
}
staggerChildren0.15s
0.05s = fast sweep → 0.6s = very slow cascade
delayChildren0.2s
0s = start immediately → 1s = long initial pause
  • Fast
  • Reliable
  • Easy to use
  • Customizable
  • Secure
ConceptWhat it does
variantsNamed animation states defined as plain objects outside JSX. Keeps markup clean.
initial="hidden"Sets the starting state by referencing the variant name — no inline object needed.
animate="visible"The target state to animate toward, again by name.
staggerChildrenDelay between each child's animation start. Creates a cascade effect.
delayChildrenExtra wait before the very first child begins animating.
inheritanceChild motion elements inherit the parent's state — no need to repeat initial/animate on each one.
Why variants? Without variants, animating a list of 10 items means writing initial, animate, and transition on every single motion.li. Variants let you define those states once and reference them by name. The parent then coordinates timing for all children automatically via staggerChildren — no manual delay calculations needed. Try dragging staggerChildren to 0.4s to see each item land individually, or 0.05s for a near-simultaneous sweep.
Drag — drag, dragConstraints & dragElastic
import { motion } from "framer-motion";

function DraggableCard() {
  return (
    <motion.div
      drag
      dragConstraints={{ left: -130, right: 130, top: -40, bottom: 40 }}
      dragElastic={0.2}
      whileDrag={{ scale: 1.08, rotate: 2 }}
    >
      Drag me!
    </motion.div>
  );
}
dragElastic0.2
0 = hard wall → 1 = fully elastic, no resistance
drag axis
true = both axes "x" = horizontal only "y" = vertical only
dragSnapToOrigin
snaps card back to starting position on release
Drag me!
PropWhat it does
dragEnables dragging. true = both axes, "x" = horizontal, "y" = vertical.
dragConstraintsLimits drag range in px from the resting position. left/right/top/bottom.
dragElasticOvershoot amount past constraints. 0 = hard stop, 1 = fully elastic.
dragSnapToOriginSnaps the element back to its original position when released.
whileDragAnimation applied while actively dragging. Auto-reverses on release.
onDragEndCallback fired on release. Receives event + info including final velocity.
Things to try: Set dragElastic to 0 for a hard boundary that stops dead, then 1 for a fully elastic feel with no resistance. Toggle dragSnapToOrigin on to make the card always return home — great for UI where dragging is a gesture (like a dismiss action) rather than a repositioning tool. Switch the axis to "x" to lock it to a horizontal slider pattern.
AnimatePresence — animating exit
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

function DismissableAlert() {
  const [open, setOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setOpen((o) => !o)}>
        Toggle alert
      </button>

      {/*
        AnimatePresence must wrap the conditional element.
        Without it, React removes it instantly — no exit animation.
      */}
      <AnimatePresence mode="sync">
        {open && (
          <motion.div
            key="alert"
            initial={{ opacity: 0, y: -20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -20 }}
            transition={{ duration: 0.25 }}
          >
            This is an animated alert.
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}
alert type
switches the alert colour — show/hide to see it re-enter
AnimatePresence mode
sync = simultaneous wait = exit finishes before enter popLayout = instant reflow
exit direction
direction the alert slides when entering and exiting
transition duration0.25s
0.1s = very snappy → 1s = slow and deliberate
Prop / conceptWhat it does
<AnimatePresence>Wrapper that intercepts unmounts and plays exit animations before removal.
exitThe animation state to reach before the element is removed from the DOM.
mode="sync"Enter and exit animations play at the same time (default).
mode="wait"Waits for the exit to fully complete before the enter begins.
mode="popLayout"Exiting element is removed from layout flow immediately so others reflow.
keyAnimatePresence tracks children by key. Changing key = exit old, enter new.
The key insight: React removes elements from the DOM synchronously — there is no "about to unmount" phase for animations to hook into. This is exactly what AnimatePresence solves. It intercepts the unmount, plays the exit animation to completion, then lets React remove the element. Try switching mode to "wait" — the alert fully disappears before reappearing, which is useful for page transitions. The key prop on the child is required so AnimatePresence knows which element to track.
AnimatePresence — view transitions & step switcher
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

function StepSwitcher() {
  const [step, setStep] = useState(1);

  return (
    <div>
      <button onClick={() => setStep((s) => (s === 1 ? 2 : 1))}>
        Toggle Step
      </button>

      {/*
        mode="wait" — step1 fully exits before step2 enters.
        Each child needs a unique key so AnimatePresence knows
        which one is leaving and which one is arriving.
      */}
      <AnimatePresence mode="wait">
        {step === 1 && (
          <motion.div
            key="step1"
            initial={{ opacity: 0, y: 10 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -10 }}
            transition={{ duration: 0.3 }}
          >
            Step 1
          </motion.div>
        )}
        {step === 2 && (
          <motion.div
            key="step2"
            initial={{ opacity: 0, y: 10 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -10 }}
            transition={{ duration: 0.3 }}
          >
            Step 2
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}
AnimatePresence mode
wait = clean handoff sync = both animate at the same time (try it — feels messier)
transition duration0.3s
slow it down to see the exit and enter phases clearly
directional animation
Next slides up, Prev slides down — spatially reinforces the flow
Step 1
Install the package
Run npm install framer-motion in your project root.
ConceptWhat it does
key per viewUnique key on each child tells AnimatePresence which is entering and which is leaving.
mode="wait"The current view exits fully before the next one enters — a clean sequential handoff.
mode="sync"Both animations run at the same time — enter and exit overlap. Often feels busier.
Mirrored y valuesexit: y:-10 mirrors initial: y:10 — content slides out the same direction it came from.
Directional exitFlip exit direction based on forward/back navigation to reinforce spatial movement.
position: absoluteBoth views occupy the same space during the transition, so they overlap cleanly.
The pattern in the wild: This is exactly how page transitions, tab panels, carousels, and onboarding wizards work in production apps. The container has position: relative and a fixed height — both views use position: absolute so they stack on top of each other during the transition rather than pushing layout around. Toggle directional animation on and click Prev to see the slides reverse direction — a small detail that makes navigation feel spatially grounded.
layout — automatic size & position animation
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";

function CollapsibleList() {
  const [expanded, setExpanded] = useState(null);
  const items = [1, 2, 3];

  return (
    <div>
      {items.map((id) => (
        /*
          layout — when this card's height changes (content appears
          or disappears inside), Framer Motion smoothly animates
          the resize. No height animation code needed.
        */
        <motion.div
          key={id}
          layout
          onClick={() => setExpanded((prev) => (prev === id ? null : id))}
        >
          <h3>Item {id}</h3>

          {/*
            AnimatePresence handles the content fade-out.
            layout on the parent handles the card resize.
            Together they produce a smooth accordion effect.
          */}
          <AnimatePresence>
            {expanded === id && (
              <motion.p
                layout
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
              >
                Expanded content here.
              </motion.p>
            )}
          </AnimatePresence>
        </motion.div>
      ))}
    </div>
  );
}
layout type
true = animate size + position "position" = position only "size" = size only
spring stiffness300
50 = slow and loose → 600 = very snappy resize
spring damping30
5 = bouncy → 60 = no bounce, settles immediately
What is layout animation?
When should I use it?
Does it work with AnimatePresence?
Prop / conceptWhat it does
layoutAutomatically animates any size or position change on this element using FLIP.
layout="position"Only animates position changes — ignores size. Good for repositioning items.
layout="size"Only animates size changes — ignores position. Good for expanding in-place.
layoutIdShared identity across renders. Framer Motion morphs one element into another — used for shared element transitions.
transition.layoutControls the speed/easing of layout animations separately from other transitions.
FLIP techniqueMeasures DOM before and after change, plays a transform between the two states. No height tweening needed.
Why layout is special: Before layout, animating an element's height meant manually tracking pixel values, using max-height hacks, or reaching for a library. Framer Motion's FLIP technique sidesteps all of that — it snapshots the element before and after the DOM change, then plays a smooth transform between the two. Try setting damping to 5 for a bouncy accordion, or stiffness to 600 for an instant-feeling but still-smooth snap. Switch layout type to "position" to see cards reflow without resizing.
Keyframes & repeat — looping animations
import { motion } from "framer-motion";

function LiveBadge() {
  return (
    <motion.span
      animate={{
        // Array values = keyframe sequence: start → peak → back
        scale: [1, 1.15, 1],
        boxShadow: [
          "0 0 0px rgba(124, 58, 237, 0.3)",
          "0 0 12px rgba(124, 58, 237, 0.8)",
          "0 0 0px rgba(124, 58, 237, 0.3)",
        ],
      }}
      transition={{
        duration: 1.2,       // one full pulse cycle
        repeat: Infinity,    // loop forever
        repeatType: "reverse", // play forwards then backwards
        repeatDelay: 3,      // rest 3s between each pulse
        ease: "easeInOut",
      }}
    >
      ● LIVE
    </motion.span>
  );
}
colour
glow colour is derived from the badge background
repeatType
reverse = fwd then back loop = restart from beginning mirror = reverse with mirrored easing
scale peak1.15
how large the badge grows at the peak of the pulse
duration1.2
0.3s = fast flicker → 3s = slow deep breath
repeatDelay3
0s = continuous pulse → 5s = long rest between pulses
● LIVE
Prop / conceptWhat it does
Keyframe arrayPass an array of values to animate through multiple states: [1, 1.15, 1].
repeat: InfinityLoops the animation forever. Pass a number to loop a fixed number of times.
repeatType: "loop"Restarts from the first keyframe each time. Can feel abrupt on non-symmetrical animations.
repeatType: "reverse"Plays forwards then backwards, back and forth. Produces a smooth breathing effect.
repeatType: "mirror"Like reverse but the easing curve is also mirrored on the way back.
repeatDelayPause in seconds between each cycle. Creates a rest between pulses.
boxShadow keyframesFramer Motion can animate box-shadow arrays. All values must share the same shadow structure.
Keyframes vs A→B: Regular Framer Motion animations go from initial to animate — one value to another. Keyframe arrays let you chain multiple stops in a single animation: scale: [1, 1.15, 1] is a full pulse in one property. Try setting repeatDelay to 0 for a continuous throb, then 3 for a subtle periodic reminder. Switch repeatType to "loop" to see why"reverse" feels smoother — loop restarts abruptly, reverse eases back naturally. The boxShadow glow is animated the same way — just make sure all array values use identical shadow syntax or Framer Motion can't interpolate between them.