Skip to content

Comments

Fix AnimatePresence exit animations with Radix UI asChild#3460

Merged
mattgperry merged 1 commit intomainfrom
fix-animate-presence-radix-v2
Jan 7, 2026
Merged

Fix AnimatePresence exit animations with Radix UI asChild#3460
mattgperry merged 1 commit intomainfrom
fix-animate-presence-radix-v2

Conversation

@mattgperry
Copy link
Collaborator

Summary

  • Fixes AnimatePresence exit animations not running when using Radix UI's asChild prop with motion components
  • Stores externalRef in a ref (updated via useInsertionEffect for concurrent mode safety) instead of including it in useCallback dependency array
  • Preserves React 19 cleanup function support

Problem

When using Radix UI Dialog (or similar libraries) with asChild and motion components inside AnimatePresence, exit animations were not running. The elements were immediately removed from the DOM instead of animating out.

<AnimatePresence>
  {isOpen ? (
    <DialogPrimitive.Portal forceMount>
      <DialogPrimitive.Overlay asChild>
        <motion.div exit={{ opacity: 0 }} />  {/* Exit animation not working */}
      </DialogPrimitive.Overlay>
    </DialogPrimitive.Portal>
  ) : null}
</AnimatePresence>

Root Cause

In use-motion-ref.ts, externalRef was in the useCallback dependency array. Radix UI's asChild creates a new composed callback ref on each render. This caused:

  1. useCallback to return a new function each render
  2. React to call the ref with null (unmount) then the new instance (remount)
  3. AnimatePresence exit animations to be bypassed

Solution

Store externalRef in a ref (externalRefContainer) and update it via useInsertionEffect (for concurrent mode safety). Access the current value inside the callback rather than including it in dependencies. This maintains:

  • React 19 cleanup function support
  • Proper ref forwarding
  • AnimatePresence exit animations working correctly

Test plan

Fixes #3455

🤖 Generated with Claude Code

When using Radix UI's asChild prop with motion components inside
AnimatePresence, exit animations were not running. This was because
externalRef was in the useMotionRef useCallback dependency array,
causing the callback to be recreated whenever Radix created a new
composed ref on render.

The fix stores externalRef in a ref (updated via useInsertionEffect
for concurrent mode safety) to access the current value without
including it in the dependency array. This preserves:
- React 19 cleanup function support
- Proper ref forwarding
- AnimatePresence exit animations with libraries like Radix UI

Added E2E tests with @radix-ui/react-dialog to verify the fix.

Fixes #3455

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@mattgperry mattgperry merged commit 40e0632 into main Jan 7, 2026
3 of 4 checks passed
@mattgperry mattgperry deleted the fix-animate-presence-radix-v2 branch January 7, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] - Possible AnimatePresence regression in 12.24.4

1 participant