使用Framer Motion实现的自定义悬停光标抖动问题求助
Framer Motion实现的自定义悬停光标抖动问题求助
我现在想复刻那些鼠标悬停时的酷炫自定义光标效果,用的是Framer Motion。逻辑大概是:当鼠标悬停在一个div上时,默认光标消失,取而代之的是一个中等大小的圆形跟着鼠标移动。我已经实现了基本功能,但这个圆形的移动特别卡顿,而且看起来还总是想回到角落位置。
我的组件代码:
"use client"; import { useState } from "react"; import { motion } from "framer-motion"; type Position = { x: number; y: number; }; function BigBlockImage() { const [isHovering, setIsHovering] = useState(false); const [position, setPosition] = useState<Position>({ x: 0, y: 0 }); return ( <div className="w-full aspect-video bg-placeholder relative cursor-none" onMouseEnter={(e) => { setIsHovering(true); }} onMouseLeave={(e) => { setIsHovering(false); }} onMouseMove={(e) => { const rect = e.target.getBoundingClientRect(); const x = e.clientX - rect.left - 57; const y = e.clientY - rect.top - 57; console.log(x, y); setPosition({ x, y, }); }} > {isHovering && ( <motion.div animate={{ x: position.x, y: position.y }} transition={{ type: "tween", ease: "backOut" }} className="absolute w-[114px] aspect-square bg-accent rounded-full flex justify-center items-center uppercase text-primary pt-1" > VIEW </motion.div> )} </div> ); } export default BigBlockImage;
我不确定是因为短时间内多次设置状态导致的问题,还是我的实现方式本身就错了。希望能得到大家的帮助,谢谢!
问题分析与优化方案
我来帮你拆解下问题的核心原因,然后给出针对性的优化方案:
1. 核心问题所在
- 频繁状态更新触发重渲染:
onMouseMove会高频触发,每次调用setPosition都会让组件重新渲染,再加上Framer Motion的tween动画有过渡延迟,就会出现光标追不上鼠标、抖动的情况。 - 坐标计算与动画的冲突:你手动减去57来居中光标,再配合
backOut的缓动效果,会让光标在移动时出现“回弹”的错觉,看起来像是要回到角落。 - 鼠标事件目标的不确定性:如果后续容器内添加子元素,
e.target可能会变成子元素,导致getBoundingClientRect()获取的不是容器的坐标,进一步加剧抖动。
2. 优化后的代码实现
"use client"; import { useState, useRef } from "react"; import { motion, useMotionValue, useSpring } from "framer-motion"; function BigBlockImage() { const [isHovering, setIsHovering] = useState(false); // 用ref锁定容器元素,确保坐标计算的准确性 const containerRef = useRef<HTMLDivElement>(null); // 用Framer Motion的MotionValue存储位置,避免状态更新触发重渲染 const mouseX = useMotionValue(0); const mouseY = useMotionValue(0); // 用Spring动画让光标移动更顺滑,跟得上鼠标速度 const smoothX = useSpring(mouseX, { stiffness: 400, damping: 30 }); const smoothY = useSpring(mouseY, { stiffness: 400, damping: 30 }); return ( <div ref={containerRef} className={`w-full aspect-video bg-placeholder relative ${isHovering ? "cursor-none" : ""}`} onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} onMouseMove={(e) => { const rect = containerRef.current?.getBoundingClientRect(); if (!rect) return; // 直接计算鼠标相对于容器的坐标,用CSS translate居中光标,不用手动减偏移量 const x = e.clientX - rect.left; const y = e.clientY - rect.top; mouseX.set(x); mouseY.set(y); }} > {isHovering && ( <motion.div style={{ x: smoothX, y: smoothY, // 用translate(-50%, -50%)实现光标居中,计算更简单 transform: "translate(-50%, -50%)" }} // 禁用光标元素的鼠标事件,避免干扰容器的鼠标监听 className="absolute w-[114px] aspect-square bg-accent rounded-full flex justify-center items-center uppercase text-primary pt-1 pointer-events-none" > VIEW </motion.div> )} </div> ); } export default BigBlockImage;
3. 关键优化点说明
- 用MotionValue替代State:Framer Motion的
useMotionValue可以直接驱动动画,不会像React State那样触发组件重渲染,性能更高,动画更顺滑。 - Spring动画过渡:替换原来的
tween+backOut为spring动画,让光标能快速跟上鼠标的移动,同时保持顺滑的过渡效果,不会出现卡顿。 - CSS居中替代手动计算:用
transform: translate(-50%, -50%)让光标自动居中,不用手动计算偏移量,避免坐标错误。 - 锁定容器Ref:确保
getBoundingClientRect()始终获取的是容器的坐标,不会因为子元素干扰而出错。 - 添加pointer-events-none:避免自定义光标元素干扰容器的鼠标事件监听,防止坐标计算异常。
额外小建议
- 可以把
stiffness和damping参数调整到你觉得最舒服的数值,数值越大光标跟手性越强。 - 移除代码中的
console.log,减少不必要的性能消耗。
备注:内容来源于stack exchange,提问作者mizzadnan




