You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

使用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+backOutspring动画,让光标能快速跟上鼠标的移动,同时保持顺滑的过渡效果,不会出现卡顿。
  • CSS居中替代手动计算:用transform: translate(-50%, -50%)让光标自动居中,不用手动计算偏移量,避免坐标错误。
  • 锁定容器Ref:确保getBoundingClientRect()始终获取的是容器的坐标,不会因为子元素干扰而出错。
  • 添加pointer-events-none:避免自定义光标元素干扰容器的鼠标事件监听,防止坐标计算异常。

额外小建议

  • 可以把stiffnessdamping参数调整到你觉得最舒服的数值,数值越大光标跟手性越强。
  • 移除代码中的console.log,减少不必要的性能消耗。

备注:内容来源于stack exchange,提问作者mizzadnan

火山引擎 最新活动