Exit Animations: Page Transitions and Unmount Effects
Without exit animations, elements vanish instantly when unmounted—jarring and unfinished. Framer Motion's AnimatePresence component delays unmounting until an exit animation completes, creating smooth transitions when elements are removed from the DOM. This is essential for dismissible modals, toasts, lists with delete actions, and page transitions.
I've used AnimatePresence on every React project since learning it, because the pattern—animate-then-unmount—is so fundamental to polished UX. This article covers how to set it up and the patterns that make exit animations shine.
Understanding AnimatePresence
AnimatePresence is a utility component that preserves motion elements in the DOM while their exit animations play. Without it, elements unmount synchronously, and exit props are ignored.
Here's the difference:
Without AnimatePresence (element unmounts instantly):
import { motion } from 'framer-motion';
import React from 'react';
export function WithoutAnimatePresence() {
const [showBox, setShowBox] = React.useState(true);
return (
<div>
<button onClick={() => setShowBox(!showBox)}>
{showBox ? 'Hide' : 'Show'}
</button>
{/* Box unmounts instantly—no exit animation */}
{showBox && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
style={{
padding: '20px',
backgroundColor: '#3498db',
color: '#fff',
borderRadius: '8px'
}}
>
This box disappears instantly.
</motion.div>
)}
</div>
);
}
Click "Hide" and the box vanishes with no animation.
With AnimatePresence (element exits smoothly):
import { motion, AnimatePresence } from 'framer-motion';
import React from 'react';
export function WithAnimatePresence() {
const [showBox, setShowBox] = React.useState(true);
return (
<div>
<button onClick={() => setShowBox(!showBox)}>
{showBox ? 'Hide' : 'Show'}
</button>
{/* AnimatePresence delays unmount for exit animation */}
<AnimatePresence>
{showBox && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
style={{
padding: '20px',
backgroundColor: '#3498db',
color: '#fff',
borderRadius: '8px'
}}
>
This box fades out smoothly.
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Click "Hide" and the box fades out before unmounting. Much better.
Exit Animation for Lists and Deletions
The most common use case is animating out list items when they're deleted:
import { motion, AnimatePresence } from 'framer-motion';
import React from 'react';
export function DeletableList() {
const [items, setItems] = React.useState([
{ id: 1, label: 'Learn React' },
{ id: 2, label: 'Learn Framer Motion' },
{ id: 3, label: 'Build a project' }
]);
const handleDelete = (id) => {
setItems((prev) => prev.filter((item) => item.id !== id));
};
return (
<AnimatePresence>
<ul style={{ listStyle: 'none', padding: 0 }}>
{items.map((item) => (
<motion.li
key={item.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ duration: 0.3 }}
style={{
padding: '12px',
borderBottom: '1px solid #eee',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<span>{item.label}</span>
<button
onClick={() => handleDelete(item.id)}
style={{
padding: '6px 12px',
backgroundColor: '#e74c3c',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Delete
</button>
</motion.li>
))}
</ul>
</AnimatePresence>
);
}
When you click delete, the item slides out to the right (x: 100) while fading out (opacity: 0) before being removed from the list. The animation takes 0.3 seconds.
Exit Animations for Modals and Overlays
Modals should exit smoothly with a fade and scale-down effect:
import { motion, AnimatePresence } from 'framer-motion';
import React from 'react';
export function ModalWithExit() {
const [isOpen, setIsOpen] = React.useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? 'Close Modal' : 'Open Modal'}
</button>
<AnimatePresence>
{isOpen && (
<>
{/* Overlay backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={() => setIsOpen(false)}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 999
}}
/>
{/* Modal content */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: '#fff',
padding: '30px',
borderRadius: '12px',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
zIndex: 1000,
maxWidth: '500px'
}}
>
<h2>Modal Title</h2>
<p>This modal fades and scales when opened/closed.</p>
<button
onClick={() => setIsOpen(false)}
style={{
padding: '10px 20px',
backgroundColor: '#3498db',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Close
</button>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}
Both the backdrop and modal content have exit animations. The backdrop fades out quickly (0.2s), while the modal fades and scales down with a spring transition.
Page Transitions with Exit Animations
For page transitions (e.g., in Next.js or React Router), animate pages exiting and entering:
import { motion, AnimatePresence } from 'framer-motion';
import React from 'react';
const pageVariants = {
initial: { opacity: 0, y: 20 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -20 }
};
export function PageTransition({ page }) {
return (
<AnimatePresence mode="wait">
<motion.div
key={page}
variants={pageVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.4 }}
style={{ padding: '20px' }}
>
<h1>{page}</h1>
<p>Page content for {page}</p>
</motion.div>
</AnimatePresence>
);
}
export function App() {
const [page, setPage] = React.useState('Home');
return (
<div>
<div style={{ marginBottom: '20px' }}>
<button onClick={() => setPage('Home')}>Home</button>
<button onClick={() => setPage('About')}>About</button>
<button onClick={() => setPage('Contact')}>Contact</button>
</div>
<PageTransition page={page} />
</div>
);
}
The mode="wait" prop on AnimatePresence ensures the exiting page completes its animation before the new page starts. Without it, both animations run simultaneously, which can look chaotic.
Toast Notifications with Exit Animations
Toasts should exit automatically with a slide and fade:
import { motion, AnimatePresence } from 'framer-motion';
import React from 'react';
export function ToastContainer() {
const [toasts, setToasts] = React.useState([]);
const addToast = (message) => {
const id = Date.now();
setToasts((prev) => [...prev, { id, message }]);
// Auto-remove after 3 seconds
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 3000);
};
return (
<div>
<button onClick={() => addToast('Success!')}>Add Toast</button>
<AnimatePresence>
<div
style={{
position: 'fixed',
top: '20px',
right: '20px',
zIndex: 9999
}}
>
{toasts.map((toast) => (
<motion.div
key={toast.id}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ duration: 0.3 }}
style={{
padding: '15px 20px',
backgroundColor: '#27ae60',
color: '#fff',
borderRadius: '6px',
marginBottom: '10px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)'
}}
>
{toast.message}
</motion.div>
))}
</div>
</AnimatePresence>
</div>
);
}
Each toast slides in from the right and fades in. After 3 seconds, it slides out and fades out before unmounting.
AnimatePresence mode Prop
Control how multiple animations interact with the mode prop:
mode="sync"(default): All animations run simultaneously.mode="wait": Exit animations complete before enter animations start.mode="popLayout": Exit animations don't affect layout.
Use mode="wait" for page transitions and modals. Use sync for lists where items exit and enter independently.
Key Takeaways
AnimatePresencedelays unmounting to allow exit animations to complete.- Wrap conditional elements with
AnimatePresenceto enableexitprop behavior. - Use
exitanimations to slide, fade, or scale elements out before removal. - For page transitions, use
mode="wait"to ensure sequential animations. - Combine entrance and exit animations with variants for consistent, choreographed transitions.
Frequently Asked Questions
Can I use exit without AnimatePresence?
No. Without AnimatePresence, the component unmounts synchronously and the exit animation never plays. AnimatePresence is required for exit animations to work.
How do I make an element exit in one direction and enter in another?
Define different initial and exit states: initial={{ x: -100 }}, exit={{ x: 100 }}. On enter, it slides in from the left; on exit, it slides out to the right.
Can I nest AnimatePresence components?
Yes. You can have AnimatePresence at multiple levels (page level, modal level, list level) for complex animations.
How do I know when the exit animation has completed?
Use onAnimationComplete callback on the motion element: onAnimationComplete={() => console.log('done')}. It fires when the animate prop finishes (or the exit animation completes).