Skip to content

[BUG] AnimatePresence keeps exiting children in DOM during rapid updates with dynamic variants #3541

@xellanix

Description

@xellanix

Note

Motion for Vue issues: Please open in the Motion for Vue repo.

1. Read the FAQs 👇

2. Describe the bug

When switching children inside AnimatePresence rapidly (triggering updates before the previous exit animation completes), exiting children frequently get stuck in the DOM.

Specific observations:

  • Stuck Elements: The "Child count" remains greater than 1 even after visual animations cease.
  • Incorrect Exit State: The non-active (stuck) children seem to apply the exit state of the currently active child's variant.
  • Callback Failure: onExitComplete fails to fire for these stuck elements.
  • Recovery: The state only recovers (DOM clears, onExitComplete fires) after specific subsequent triggers, though the exact trigger point is inconsistent.

Context: This issue seems prominent when using dynamic variants where the animated properties change between renders (e.g., Variant A uses opacity, while Variant B uses transform).

3. IMPORTANT: Provide a CodeSandbox reproduction of the bug

https://codesandbox.io/p/sandbox/7jcg3l

I have included debugging tools in the UI ("Child count" indicator and "Spam" simulation buttons) to make reproduction easier.

4. Steps to reproduce

Steps to reproduce the behavior:

  1. Open the provided CodeSandbox link.
  2. Click the "Spam Move 4x" button (this simulates 4 clicks with 100ms intervals) OR manually spam click the "Move" button rapidly.
  3. Observe the "Child count" indicator in the UI.
  4. See error: The "Child count" shows a number greater than 1 (e.g., 2 or 3) after the animation visually finishes, indicating children are stuck in the DOM.
  5. Check the log: No new onExitComplete logs are generated during the bugged state.

5. Expected behavior

  • The "Child count" should always return to 1 after the transition sequence completes.
  • onExitComplete should fire reliably for every exiting component.
  • Exiting children should be removed from the DOM regardless of how fast the state updates occur.

6. Video or screenshots

Video 1: Bug Reproduction
https://github.com/user-attachments/assets/bb7f4ca4-b5f5-49c0-9919-711d85d2c0d7
Description: I trigger the bug using "Spam Move 4x". The child count remains stuck. I then click "Move" slowly until the system recovers (count returns to 1 and logs appear), then I trigger the bug again.

Video 2: Normal Behavior
https://github.com/user-attachments/assets/4a26eae3-ac9e-488b-9f0f-0ca1a4ce24ce

7. Environment details

CodeSandbox Environment:

  • React: 18.2.0
  • Motion: 12.33.0

Local Environment (Reproduction confirmed here too):

  • OS: Windows 11 Home 24H2
  • Browser: Zen (Firefox based) & Microsoft Edge
  • React: 19.2.3
  • Motion: 12.29.2

FAQs

React Server Components "use client" error

If you're importing motion or m into a React Server Component environment, ensure you're importing from motion/react-client instead of motion/react.

import * as motion from "motion/react-client"
import * as m from "framer-motion/react-m"

Motion for React won't install

Different versions of Motion for React are compatible with different versions of React.

React 19: framer-motion@12.0.0-alpha.0 or higher
React 18: framer-motion@7.0.0 to framer-motion@11.x, or motion
React 17: framer-motion@6.x or lower

height: "auto" is jumping

Animating to/from auto requires measuring the DOM. There's no perfect way to do this and if you have also applied padding to the same element, these measurements might be wrong.

The recommended solution is to move padding to a child element. See this issue for the full discussion.

Preact isn't working

Motion for React isn't compatible with Preact.

AnimatePresence isn't working

Have all of its immediate children got a unique key prop that remains the same for that component every render?

// Bad: The index could be given to a different component if the order of items changes
<AnimatePresence>
    {items.map((item, index) => (
        <Component key={index} />
    ))}
</AnimatePresence>
// Good: The item ID is unique to each component
<AnimatePresence>
    {items.map((item, index) => (
        <Component key={item.id} />
    ))}
</AnimatePresence>

Is the AnimatePresence correctly outside of the controlling conditional? AnimatePresence must be rendered whenever you expect an exit animation to run - it can't do so if it's unmounted!

// Bad: AnimatePresence is unmounted - exit animations won't run
{
    isVisible && (
        <AnimatePresence>
            <Component />
        </AnimatePresence>
    )
}
// Good: Only the children are unmounted - exit animations will run
<AnimatePresence>{isVisible && <Component />}</AnimatePresence>

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions