DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

Framer Motion / Motion: React Animations Complete Guide (2026)

The library formerly known as Framer Motion is now just motion. Same team, same API, leaner package. The patterns that were experimental in v10 are stable, and scroll-driven animations are now the standard.

Most animation tutorials show a spinning box. This guide shows what you actually reach for: exit animations, shared layout transitions, scroll effects, stagger lists, gesture interactions.

Installation

npm install motion
# or: npm install framer-motion (still works, re-exports from motion)
Enter fullscreen mode Exit fullscreen mode
import { motion, AnimatePresence } from 'motion/react'
Enter fullscreen mode Exit fullscreen mode

The motion Component

<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  exit={{ opacity: 0, y: -20 }}
  transition={{ duration: 0.4, ease: 'easeOut' }}
>
  Content
</motion.div>
Enter fullscreen mode Exit fullscreen mode

Stagger with Variants

const container = {
  hidden: {},
  visible: { transition: { staggerChildren: 0.08 } },
}
const item = {
  hidden: { opacity: 0, y: 16 },
  visible: { opacity: 1, y: 0, transition: { duration: 0.35 } },
}

function AnimatedList({ items }: { items: string[] }) {
  return (
    <motion.ul variants={container} initial="hidden" animate="visible">
      {items.map(i => <motion.li key={i} variants={item}>{i}</motion.li>)}
    </motion.ul>
  )
}
Enter fullscreen mode Exit fullscreen mode

Children animate 80ms apart, automatically.

AnimatePresence: Exit Animations

React unmounts instantly — AnimatePresence lets the exit prop play before removal:

<AnimatePresence>
  {toasts.map(toast => (
    <motion.div
      key={toast.id}
      initial={{ opacity: 0, x: 100 }}
      animate={{ opacity: 1, x: 0 }}
      exit={{ opacity: 0, x: 100 }}
      transition={{ type: 'spring', stiffness: 300, damping: 30 }}
    >
      {toast.message}
    </motion.div>
  ))}
</AnimatePresence>
Enter fullscreen mode Exit fullscreen mode

For switching between two components, use mode="wait":

<AnimatePresence mode="wait">
  <motion.div key={currentTab} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
    {currentTab === 'a' ? <TabA /> : <TabB />}
  </motion.div>
</AnimatePresence>
Enter fullscreen mode Exit fullscreen mode

Spring Physics

// Snappy, no bounce
transition={{ type: 'spring', stiffness: 400, damping: 30 }}

// Bouncy
transition={{ type: 'spring', stiffness: 200, damping: 10 }}

// Slow, heavy
transition={{ type: 'spring', stiffness: 80, damping: 20 }}
Enter fullscreen mode Exit fullscreen mode

Layout Animations

The layout prop animates position/size changes when the component moves in the DOM:

<AnimatePresence>
  {filtered.map(item => (
    <motion.li
      key={item.id}
      layout                      // ← smoothly repositions when filter changes
      initial={{ opacity: 0, scale: 0.8 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.8 }}
    >
      <ItemCard item={item} />
    </motion.li>
  ))}
</AnimatePresence>
Enter fullscreen mode Exit fullscreen mode

layoutId: Shared Layout Morphing

The most impressive feature — elements with the same layoutId morph between each other's positions:

// In the grid
<motion.div layoutId={`card-${image.id}`} onClick={() => setSelected(image)}>
  <motion.img layoutId={`img-${image.id}`} src={image.src} />
</motion.div>

// In the modal (completely different position in the tree)
<motion.div layoutId={`card-${image.id}`} className="fixed inset-4">
  <motion.img layoutId={`img-${image.id}`} src={image.src} />
</motion.div>
Enter fullscreen mode Exit fullscreen mode

The card morphs from grid to modal. Zero position calculation needed.

Scroll-Driven Animations

import { useScroll, useTransform } from 'motion/react'

function ParallaxSection() {
  const ref = useRef<HTMLDivElement>(null)
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ['start start', 'end start'],
  })

  const y = useTransform(scrollYProgress, [0, 1], ['0%', '30%'])
  const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0])

  return (
    <div ref={ref} className="relative h-[80vh] overflow-hidden">
      <motion.div style={{ y, opacity }} className="absolute inset-0 bg-gradient-to-b from-blue-900 to-purple-900" />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Animate on Scroll Into View

import { useInView } from 'motion/react'

function AnimateOnScroll({ children }: { children: React.ReactNode }) {
  const ref = useRef<HTMLDivElement>(null)
  const isInView = useInView(ref, { once: true, margin: '-100px 0px' })

  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 40 }}
      animate={isInView ? { opacity: 1, y: 0 } : {}}
      transition={{ duration: 0.6 }}
    >
      {children}
    </motion.div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Gesture Animations

<motion.div
  whileHover={{ scale: 1.02, y: -4 }}
  whileTap={{ scale: 0.98 }}
  transition={{ type: 'spring', stiffness: 400, damping: 17 }}
  className="cursor-pointer rounded-xl border p-6"
>
  Card content
</motion.div>
Enter fullscreen mode Exit fullscreen mode

SVG Path Animation

<motion.path
  d="M14 27 L21 34 L38 17"
  fill="none" stroke="currentColor" strokeWidth="3"
  initial={{ pathLength: 0 }}
  animate={{ pathLength: 1 }}
  transition={{ duration: 0.4, delay: 0.5 }}
/>
Enter fullscreen mode Exit fullscreen mode

Performance

// ✅ GPU-accelerated (fast)
animate={{ x: 100, scale: 1.1, opacity: 0.8 }}

// ❌ Triggers layout recalculation (slow)
animate={{ width: 200, height: 100 }}

// Respect prefers-reduced-motion
const shouldReduce = useReducedMotion()
<motion.div initial={{ y: shouldReduce ? 0 : 40 }} animate={{ y: 0 }} />
Enter fullscreen mode Exit fullscreen mode

Quick Reference

// Basic
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} />

// Spring
transition={{ type: 'spring', stiffness: 300, damping: 25 }}

// Stagger children
const container = { visible: { transition: { staggerChildren: 0.08 } } }

// Exit animations
<AnimatePresence mode="wait"><motion.div key={id} exit={{ opacity: 0 }} /></AnimatePresence>

// Shared layout
<motion.div layoutId="hero-image" />  // same ID in grid and modal

// Scroll-driven
const { scrollYProgress } = useScroll({ target: ref })
const opacity = useTransform(scrollYProgress, [0, 1], [1, 0])

// Gestures
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} />
Enter fullscreen mode Exit fullscreen mode

The patterns with the biggest impact: AnimatePresence for conditional rendering, layoutId for elements that morph between states, and staggerChildren for list entrances.


Full article at stacknotice.com/blog/framer-motion-react-animations-2026

Top comments (2)

Collapse
 
frank_signorini profile image
Frank

I'm curious about the impact of the package size reduction on performance, do you have any benchmarks? I'd love to swap ideas on optimizing motion animations.

Collapse
 
stacknotice profile image
Carlos Oliva Pascual

Good question — the bundle impact is actually one of the most underrated reasons to migrate from framer-motion to motion.

The full framer-motion package is around 106 KB minified (roughly 34 KB gzipped). The newer motion package improves on that through better tree-shaking, but the biggest optimization comes from LazyMotion, which many migration guides barely mention.

LazyMotion + m component is the pattern that really matters:

import { LazyMotion, domAnimation, m } from "motion/react";

// Wrap your app or section

{/* Use instead of */}

domAnimation is about 17 KB gzipped and covers roughly 95% of real-world use cases: basic animations, exit animations, and gestures. domMax adds heavier features such as layout animations and drag support, bringing the size to around 34 KB gzipped.

If you only need those advanced features on certain routes, you can lazy-load them:

const loadFeatures = () =>
import("motion/react").then((m) => m.domMax);


...

In practice, switching from to LazyMotion + with domAnimation in a Next.js application can reduce the motion-related JavaScript from roughly 34 KB to 17 KB gzipped—about a 50% reduction for that part of the main bundle.

For published benchmarks, Matt Perry (the library's author) shared size comparisons when motion was introduced. The main takeaway is that selective feature loading with LazyMotion typically reduces the animation bundle by around 40–60%, depending on which features your application actually uses.

To measure the impact in your own project, the most reliable approach is to compare bundle analyses before and after the migration using next build --analyze (or a tool such as Bundle Buddy), since the exact savings depend on which Motion APIs you're using.

Out of curiosity, are your animations mostly simple entrance/fade effects, or do you rely on heavier features like layoutId, layout transitions, or drag interactions?