I'm experiencing a performance issue where a React animation component stops working properly when a custom cursor moves around the page. The two animations seem to interfere with each other.
I have two separate animation systems running simultaneously:
When the cursor moves, the text animation appears to pause or stutter, disrupting the timing cycle.
"use client";
import React, { useCallback, useEffect, useState } from "react";
import { AnimatePresence, motion } from "motion/react";
export const AnimatedText = ({ items, duration = 3000 }) => {
const [currentItem, setCurrentItem] = useState(items[0]);
const [isAnimating, setIsAnimating] = useState(false);
const startAnimation = useCallback(() => {
const nextItem = items[items.indexOf(currentItem) + 1] || items[0];
setCurrentItem(nextItem);
setIsAnimating(true);
}, [currentItem, items]);
useEffect(() => {
if (!isAnimating) {
setTimeout(() => {
startAnimation();
}, duration);
}
}, [isAnimating, duration, startAnimation]);
return (
<AnimatePresence onExitComplete={() => setIsAnimating(false)}>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -40, x: 40, filter: "blur(8px)", scale: 2 }}
transition={{ type: "spring", stiffness: 100, damping: 10 }}
key={currentItem}
>
{currentItem.split(" ").map((word, wordIndex) => (
<motion.span
key={word + wordIndex}
initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{ delay: wordIndex * 0.3, duration: 0.3 }}
>
{word.split("").map((letter, letterIndex) => (
<motion.span
key={word + letterIndex}
initial={{ opacity: 0, y: 10, filter: "blur(8px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{
delay: wordIndex * 0.3 + letterIndex * 0.05,
duration: 0.2,
}}
>
{letter}
</motion.span>
))}
</motion.span>
))}
</motion.div>
</AnimatePresence>
);
};
import { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
export const CustomCursor = () => {
const cursorRef = useRef(null);
useEffect(() => {
const handleMouseMove = (e) => {
gsap.to(cursorRef.current, {
x: e.clientX,
y: e.clientY,
duration: 0.3,
ease: "power2.out"
});
};
document.addEventListener('mousemove', handleMouseMove);
return () => document.removeEventListener('mousemove', handleMouseMove);
}, []);
return (
<div
ref={cursorRef}
style={{
position: 'fixed',
width: '20px',
height: '20px',
backgroundColor: '#000',
borderRadius: '50%',
pointerEvents: 'none',
zIndex: 9999,
}}
/>
);
};
Expected Behavior: Both animations should run independently without interfering with each other.
Actual Behavior: The text animation timing gets disrupted when the cursor moves, causing pauses or stutters in the animation cycle.
Environment: React 18, Next.js 14, Framer Motion 11+, GSAP 3.12+, TypeScript.
- What causes this interference between Framer Motion and GSAP animations?
- How to fix this issue?
When using GSAP inside React's useEffect, it's recommended to wrap your animations inside a GSAP context to scope them properly and clean up automatically, preventing memory leaks. Alternatively, you can use a ready-made hook like useGsap from @gsap/react which handles this pattern for you.
https://gsap.com/docs/v3/GSAP/gsap.context()/
https://gsap.com/resources/React/
For mouse movement animations with GSAP, it's best to use either quickSetter or quickTo:
Use quickTo if you want smooth movement with easing
https://gsap.com/docs/v3/GSAP/gsap.quickTo()/
For maximum performance, use quickSetter. It updates the value immediately without animation, perfect for fast, direct updates like tracking the cursor position.
https://gsap.com/docs/v3/GSAP/gsap.quickSetter()
example using quickTo with useGsap hook
import { useRef } from 'react';
import { gsap } from 'gsap';
import { useGsap } from '@gsap/react';
export const CustomCursor = () => {
const cursorRef = useRef<HTMLDivElement>(null);
useGsap(() => {
if (!cursorRef.current) return;
const moveX = gsap.quickTo(cursorRef.current, 'x', {
duration: 0.3,
ease: 'power2.out',
});
const moveY = gsap.quickTo(cursorRef.current, 'y', {
duration: 0.3,
ease: 'power2.out',
});
const handleMouseMove = (e: MouseEvent) => {
moveX(e.clientX);
moveY(e.clientY);
};
document.addEventListener('mousemove', handleMouseMove);
return () => document.removeEventListener('mousemove', handleMouseMove);
}, { scope: cursorRef });
return (
<div
ref={cursorRef}
style={{
position: 'fixed',
width: 20,
height: 20,
backgroundColor: '#000',
borderRadius: '50%',
pointerEvents: 'none',
zIndex: 9999,
transform: 'translate(-50%, -50%)',
willChange: 'transform',
}}
/>
);
};