This is one of the most viewed Motion questions on Stack Overflow, and two of the most discussed GitHub issues are about AnimatePresence getting stuck when state changes quickly.
1. Put the condition inside AnimatePresence
// wrong: AnimatePresence unmounts with the modal
{isOpen && (
<AnimatePresence>
<motion.div exit={{ opacity: 0 }} />
</AnimatePresence>
)}
// right
<AnimatePresence>
{isOpen && <motion.div key="modal" exit={{ opacity: 0 }} />}
</AnimatePresence>2. Give each child a unique key
When you swap one child for another, AnimatePresence tells them apart by key. Without a key, or with the array index, it cannot detect that the old one left.
3. Put exit on the direct child
The exit prop must be on the motion component that AnimatePresence sees as its child. If you wrap it in your own component, that component must render a motion element at the top and pass props down.
4. Route transitions
const location = useLocation();
<AnimatePresence mode="wait">
<Routes location={location} key={location.pathname}>
...
</Routes>
</AnimatePresence>5. Fast state changes
If users click faster than the exit takes, older versions could leave elements stuck. Update to the latest motion package first. Using mode="wait" or shorter exits also helps.
6. Next.js App Router
The App Router swaps layouts in ways that make page exit animations hard. Animate inside the page, or look at the View Transitions API.