Fix: Framer Motion Not Working — Animation Not Playing, Exit Animation Skipped, or Layout Shift on Mount
Quick Answer
How to fix Framer Motion issues — variants, AnimatePresence for exit animations, layout animations, useMotionValue, server component errors, and performance optimization.
The Problem
An animation defined with animate doesn’t play:
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
Content
</motion.div>
// Component appears instantly with no fade-inOr exit animations are skipped when a component unmounts:
{isVisible && (
<motion.div exit={{ opacity: 0 }}>
Content
</motion.div>
)}
// Component disappears instantly — exit animation never runsOr a layout animation causes a visible jump on first render:
<motion.div layout>
Content
</motion.div>
// Content shifts position on initial mount before settlingOr using Framer Motion in a Next.js Server Component throws:
Error: useState can only be used in a Client Component.Why This Happens
Framer Motion’s animation system has specific requirements:
- Exit animations require
AnimatePresence— when a component unmounts, React immediately removes it from the DOM. Framer Motion can’t animate something that’s already gone.AnimatePresencekeeps the component mounted until its exit animation finishes. initialis required foranimateto have something to transition from — ifinitialis not set,animateapplies immediately with no transition. Settinginitial={false}disables the mount animation entirely.- Layout animations measure the DOM —
layoutprop triggers when the component re-renders. On first render, there’s nothing to animate from, which can cause a layout jump if the surrounding content shifts position. - Framer Motion uses client-side JavaScript —
motioncomponents use hooks internally. They cannot run in React Server Components (Next.js App Router). You must add"use client"to any file that importsmotion.
Fix 1: Write Animations Correctly
import { motion } from 'framer-motion';
// Fade in on mount
<motion.div
initial={{ opacity: 0 }} // Start state
animate={{ opacity: 1 }} // End state
transition={{ duration: 0.3 }}
>
Content
</motion.div>
// Slide in from left
<motion.div
initial={{ x: -50, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
>
Content
</motion.div>
// Skip mount animation (only animate on state changes)
<motion.div
initial={false}
animate={{ opacity: isVisible ? 1 : 0 }}
>
Content
</motion.div>
// Hover and tap animations
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
>
Click me
</motion.button>
// Transition types
<motion.div
animate={{ x: 100 }}
transition={{
type: 'tween', // Linear or eased
duration: 0.3,
ease: 'easeOut',
}}
/>
<motion.div
animate={{ x: 100 }}
transition={{
type: 'spring', // Physics-based
stiffness: 300,
damping: 20,
mass: 1,
}}
/>
<motion.div
animate={{ x: 100 }}
transition={{
type: 'inertia', // Momentum-based (for drag)
velocity: 50,
}}
/>Fix 2: Fix Exit Animations with AnimatePresence
import { motion, AnimatePresence } from 'framer-motion';
// WRONG — exit animation is ignored without AnimatePresence
function Modal({ isOpen }) {
return (
<>
{isOpen && (
<motion.div exit={{ opacity: 0 }}>
Modal content
</motion.div>
)}
</>
);
}
// CORRECT — wrap conditional rendering with AnimatePresence
function Modal({ isOpen }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
key="modal" // key is required for AnimatePresence
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.2 }}
>
Modal content
</motion.div>
)}
</AnimatePresence>
);
}
// Animating list items (add/remove)
function AnimatedList({ items }) {
return (
<AnimatePresence initial={false}> // initial={false} skips mount animation
{items.map(item => (
<motion.div
key={item.id} // Stable key is required
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2 }}
>
{item.name}
</motion.div>
))}
</AnimatePresence>
);
}
// Page transitions (with React Router or Next.js)
function PageWrapper({ children }) {
return (
<AnimatePresence mode="wait"> // mode="wait" finishes exit before enter
<motion.div
key={useLocation().pathname} // Unique per page
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.25 }}
>
{children}
</motion.div>
</AnimatePresence>
);
}Fix 3: Use Variants for Orchestrated Animations
Variants let you coordinate animations across parent and children:
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1, // Each child animates 0.1s after the previous
delayChildren: 0.2, // Wait 0.2s before starting children
},
},
exit: {
opacity: 0,
transition: { staggerChildren: 0.05, staggerDirection: -1 },
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { type: 'spring', stiffness: 300, damping: 25 },
},
exit: { opacity: 0, y: -20 },
};
function AnimatedList({ items }) {
return (
<AnimatePresence>
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
exit="exit"
>
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{/* variants propagate — no need to repeat initial/animate/exit */}
{item.name}
</motion.li>
))}
</motion.ul>
</AnimatePresence>
);
}
// Dynamic variants — functions that receive custom data
const cardVariants = {
offscreen: { y: 100, opacity: 0 },
onscreen: (i: number) => ({
y: 0,
opacity: 1,
transition: { delay: i * 0.1 },
}),
};
{items.map((item, i) => (
<motion.div
key={item.id}
variants={cardVariants}
initial="offscreen"
whileInView="onscreen" // Triggers when element enters viewport
viewport={{ once: true, amount: 0.3 }}
custom={i} // Passed to variant functions
/>
))}Fix 4: Layout Animations
// layout prop — animates position and size changes
function ExpandableCard({ isExpanded, onClick }) {
return (
<motion.div
layout // Animate any layout changes
onClick={onClick}
style={{ borderRadius: 12 }} // Keep consistent to avoid jarring
>
<motion.h2 layout="position">Title</motion.h2> // Only animate position
{isExpanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Expanded content
</motion.p>
)}
</motion.div>
);
}
// Shared layout animations (elements that move between positions)
import { LayoutGroup } from 'framer-motion';
function TabBar({ tabs, activeTab, setActiveTab }) {
return (
<LayoutGroup> // Groups layout animations
<div style={{ display: 'flex' }}>
{tabs.map(tab => (
<button key={tab.id} onClick={() => setActiveTab(tab.id)}>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId="underline" // Same layoutId = shared animation
style={{ height: 2, background: 'blue' }}
/>
)}
</button>
))}
</div>
</LayoutGroup>
);
}Fix 5: useMotionValue and Gestures
import { motion, useMotionValue, useTransform, useSpring, useScroll } from 'framer-motion';
// useMotionValue — a reactive value for performant animations
function DraggableSlider() {
const x = useMotionValue(0);
// Transform motion values into other values
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0]);
const background = useTransform(x, [-200, 0, 200], ['#ff0000', '#0000ff', '#00ff00']);
return (
<motion.div
drag="x" // Enable horizontal drag
dragConstraints={{ left: -200, right: 200 }}
style={{ x, opacity, background }}
/>
);
}
// Scroll-linked animations
function ScrollProgress() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30 });
return (
<motion.div
style={{
scaleX,
position: 'fixed',
top: 0,
left: 0,
right: 0,
height: 4,
background: 'blue',
transformOrigin: '0%',
}}
/>
);
}
// Animate on scroll — element enters viewport
function FadeInOnScroll({ children }) {
return (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.5, ease: 'easeOut' }}
>
{children}
</motion.div>
);
}Fix 6: Use with Next.js App Router
// 'use client' is REQUIRED for any component using motion
// app/components/AnimatedCard.tsx
'use client';
import { motion } from 'framer-motion';
export function AnimatedCard({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
{children}
</motion.div>
);
}
// Server Component — wraps the client component
// app/page.tsx (Server Component — no 'use client')
import { AnimatedCard } from './components/AnimatedCard';
export default async function Page() {
const data = await fetchData(); // Server-side data fetch
return (
<AnimatedCard>
<h1>{data.title}</h1>
</AnimatedCard>
);
}
// Page transitions in App Router
// app/template.tsx — template re-renders on every navigation (unlike layout)
'use client';
import { motion } from 'framer-motion';
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
}Still Not Working?
Animation plays on first render but not on state changes — check that the value you’re animating actually changes. animate={{ opacity: isVisible ? 1 : 0 }} only animates when isVisible changes. If isVisible is always true, the opacity never changes. Use React DevTools to verify the prop value is actually changing.
AnimatePresence exit animation doesn’t run — the component must be a direct child of AnimatePresence (or a descendant that’s keyed consistently). Wrapping the component in another element between AnimatePresence and the motion element breaks exit tracking. Also ensure each child has a stable key prop — without it, React may recycle the same DOM node instead of unmounting and remounting.
Layout animation causes page-wide reflow — every layout prop triggers a DOM measurement. Many layout animations on the same page can be expensive. Use layout="position" for elements that only move (not resize), and wrap unrelated layout groups in separate LayoutGroup components to scope the measurements.
For related animation libraries, see Fix: React Native Reanimated Not Working and Fix: CSS Scroll Behavior Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing
How to fix Embla Carousel issues — React setup, slide sizing, autoplay and navigation plugins, loop mode, thumbnail carousels, responsive breakpoints, and vertical scrolling.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.