Skip to main content

AnimatePresence: Enter and Exit Animations

AnimatePresence is Framer Motion's component that detects when a child is removed from the React tree and allows it to animate before unmounting. Without AnimatePresence, a component removed from render disappears instantly. With AnimatePresence, you define enter and exit animations, and Motion orchestrates them.

This is critical for page transitions, modals, notifications, and any UI that appears and disappears conditionally. The component delays actual DOM removal until the exit animation completes, preserving the visual flow.

Why AnimatePresence Matters

In React, when conditional rendering removes a component (if (showModal) <Modal />), it vanishes immediately. Users see a jump, not motion. With AnimatePresence, the removed component animates out before unmounting, maintaining visual continuity.

Framer Motion 6+ uses mode to control behavior: "sync" (enter and exit happen together), "popLayout" (exited elements don't affect layout), and "wait" (exiting finishes before entering starts).

Basic Modal with AnimatePresence

Here is a modal that fades and scales in on open, fades and scales out on close:

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

export default function ModalExample() {
const [isOpen, setIsOpen] = useState(false);

return (
<div style={{ padding: '20px' }}>
<button onClick={() => setIsOpen(true)}>Open Modal</button>

<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
onClick={() => setIsOpen(false)}
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<motion.div
initial={{ scale: 0.8 }}
animate={{ scale: 1 }}
exit={{ scale: 0.8 }}
onClick={(e) => e.stopPropagation()}
style={{
backgroundColor: 'white',
padding: '30px',
borderRadius: '12px',
maxWidth: '400px',
}}
>
<h2>Modal Title</h2>
<p>Your modal content here.</p>
<button onClick={() => setIsOpen(false)}>Close</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

When isOpen becomes true, both the overlay and modal animate in (fade and scale). When isOpen becomes false, they animate out before unmounting. The AnimatePresence wrapper ensures the exit animation runs.

Multiple Children with AnimatePresence

When animating a list of items that can be added or removed individually, use the key prop to ensure Motion tracks each item:

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

export default function DynamicList() {
const [items, setItems] = useState([
{ id: 1, text: 'Learn React' },
{ id: 2, text: 'Build an App' },
]);
const [nextId, setNextId] = useState(3);

const addItem = () => {
setItems([...items, { id: nextId, text: `Task ${nextId}` }]);
setNextId(nextId + 1);
};

const removeItem = (id) => {
setItems(items.filter(item => item.id !== id));
};

return (
<div style={{ padding: '20px', maxWidth: '400px' }}>
<button onClick={addItem}>Add Task</button>

<motion.ul style={{ listStyle: 'none', marginTop: '20px', padding: 0 }}>
<AnimatePresence mode="popLayout">
{items.map((item) => (
<motion.li
key={item.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.2 }}
style={{
padding: '10px',
backgroundColor: '#f0f0f0',
marginBottom: '10px',
borderRadius: '4px',
display: 'flex',
justifyContent: 'space-between',
}}
>
<span>{item.text}</span>
<button onClick={() => removeItem(item.id)}>Delete</button>
</motion.li>
))}
</AnimatePresence>
</motion.ul>
</div>
);
}

Each task animates in from the left and out to the right. The mode="popLayout" prevents layout shift when items are removed (the container height adjusts smoothly).

Initial vs. Exit Timing

The initial state runs when a component first renders (unless initial={false}). The exit state runs when the component is conditionally removed but before unmounting. You can stagger timings for effect:

<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{
enter: { duration: 0.3, ease: 'easeOut' },
exit: { duration: 0.2, ease: 'easeIn' },
}}
>
Asymmetric timing: slower entrance, faster exit.
</motion.div>

This staggered timing feels natural; entrances feel "solid" (slower), exits feel "dismissive" (faster).

Staggering Multiple AnimatePresence Children

When multiple elements enter or exit, stagger their animations for visual polish:

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

const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
exit: {
opacity: 0,
transition: { staggerChildren: 0.05, staggerDirection: -1 },
},
};

const itemVariants = {
hidden: { opacity: 0, y: 10 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -10 },
};

export default function StaggeredList() {
const [showItems, setShowItems] = useState(true);
const items = ['Item 1', 'Item 2', 'Item 3'];

return (
<div style={{ padding: '20px' }}>
<button onClick={() => setShowItems(!showItems)}>
{showItems ? 'Hide' : 'Show'} Items
</button>

<AnimatePresence>
{showItems && (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
exit="exit"
>
{items.map((item, i) => (
<motion.div
key={i}
variants={itemVariants}
style={{
padding: '10px',
backgroundColor: '#e0e0e0',
marginTop: '10px',
borderRadius: '4px',
}}
>
{item}
</motion.div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
);
}

On enter, items cascade in with 0.1s stagger. On exit, they stagger back out faster (staggerDirection: -1 reverses the order).

Controlling Render Order with mode

The mode prop controls whether children enter before others exit or vice versa:

  • mode="sync" (default): Siblings animate independently.
  • mode="wait": Exiting children finish before entering children start.
  • mode="popLayout": Exiting children are removed from layout immediately (don't reserve space).
<AnimatePresence mode="wait">
{currentPage === 'home' && <HomePage key="home" />}
{currentPage === 'about' && <AboutPage key="about" />}
</AnimatePresence>

With mode="wait", the home page fades out completely before the about page fades in, creating a cross-fade effect rather than an overlay.

Lifecycle and Initial Mount

By default, initial runs even on the very first render. To skip initial animation (only animate on exit), set initial={false}:

<motion.div initial={false} animate={{ x: 100 }} exit={{ x: 0 }}>
This doesn't animate on mount, only on exit.
</motion.div>

This is useful for page-load animations where you don't want the first render to move.

Handling Conditional Text with AnimatePresence

Animating text changes (e.g., form validation messages) requires a unique key to force unmount/remount:

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

export default function ValidatingForm() {
const [email, setEmail] = useState('');
const isValid = email.includes('@');

return (
<div style={{ padding: '20px', maxWidth: '300px' }}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter email"
style={{ width: '100%', padding: '8px', marginBottom: '10px' }}
/>

<AnimatePresence mode="wait">
{isValid ? (
<motion.p
key="valid"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
style={{ color: 'green' }}
>
Email is valid
</motion.p>
) : (
<motion.p
key="invalid"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
style={{ color: 'red' }}
>
Invalid email
</motion.p>
)}
</AnimatePresence>
</div>
);
}

The unique key props force unmount/remount, triggering initial and exit animations.

Key Takeaways

  • AnimatePresence delays unmounting to allow exit animations to play.
  • Wrap conditionally rendered children in AnimatePresence with unique keys.
  • Use initial, animate, and exit props to define lifecycle states.
  • mode controls render order: "wait" for cross-fades, "popLayout" for smooth list updates.
  • Stagger animations with staggerChildren for polished, cascading effects.

Frequently Asked Questions

Does AnimatePresence work with React Router?

Yes. Wrap your route-rendered components in AnimatePresence and ensure each route has a unique key (tied to location.key). This ensures each page animates in and out properly.

What if exit animation doesn't run?

Ensure the component is conditionally rendered (removed from tree), not just hidden with CSS. AnimatePresence watches for removal, not visibility changes.

Can I nest AnimatePresence?

Yes. Each AnimatePresence manages its own set of children. Use nested AnimatePresence for modal and toast hierarchies.

How do I prevent layout shift during exit animation?

Use mode="popLayout" to remove exiting elements from layout flow immediately. Alternatively, fix dimensions on the container.

Can I programmatically trigger exit animation without removing the component?

No directly, but you can use AnimatePresence with a state flag and condition the render. Or manually call motion.animate() for imperative animation.

Further Reading