wui
组件

鼠标跟随磁吸 Magnetic

让按钮与图标在光标悬停时产生朝向指针的物理吸附微位移,并在光标移出后通过弹簧平滑回正。

第三方依赖 · motion

基础用法

最简单的磁吸用法。将交互按钮包裹在 Magnetic 容器内即可拥有物理磁力吸附交互:

Loading…

安装与引入

通过 CLI 自动添加组件,或手动复制源码至项目中:

pnpm dlx @wui-design/cli@latest add @wui/magnetic
安装动效依赖与工具库
pnpm add motion clsx tailwind-merge
复制组件源码到 components/ui/magnetic.tsx
components/ui/magnetic.tsx
"use client"

import * as React from "react"
import {
  motion,
  useMotionValue,
  useReducedMotion,
  useSpring,
  type HTMLMotionProps,
  type SpringOptions,
} from "motion/react"

import { cn } from "@/lib/utils"

export interface MagneticProps extends HTMLMotionProps<"div"> {
  /** How strongly the content follows the pointer. @default 0.18 */
  strength?: number
  /** Maximum movement on either axis, in pixels. @default 12 */
  maxDistance?: number
  /** Spring physics used for movement and return. */
  springConfig?: SpringOptions
  /** Disable pointer tracking without changing the layout. @default false */
  disabled?: boolean
  // Narrows motion's children type (which allows MotionValue) back to ReactNode.
  children?: React.ReactNode
}

/** Moves its content subtly toward the pointer, then springs back to rest. */
function Magnetic({
  strength = 0.18,
  maxDistance = 12,
  springConfig = { stiffness: 260, damping: 22, mass: 0.35 },
  disabled = false,
  className,
  style,
  children,
  onPointerMove,
  onPointerLeave,
  onPointerCancel,
  ...props
}: MagneticProps) {
  const reduceMotion = useReducedMotion()
  const rawX = useMotionValue(0)
  const rawY = useMotionValue(0)
  const x = useSpring(rawX, springConfig)
  const y = useSpring(rawY, springConfig)

  React.useEffect(() => {
    if (disabled || reduceMotion) {
      rawX.set(0)
      rawY.set(0)
    }
  }, [disabled, rawX, rawY, reduceMotion])

  const reset = () => {
    rawX.set(0)
    rawY.set(0)
  }

  const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
    onPointerMove?.(event)
    if (
      event.defaultPrevented ||
      disabled ||
      reduceMotion ||
      event.pointerType === "touch"
    ) {
      return
    }

    const rect = event.currentTarget.getBoundingClientRect()
    const offsetX = (event.clientX - (rect.left + rect.width / 2)) * strength
    const offsetY = (event.clientY - (rect.top + rect.height / 2)) * strength

    const limit = Math.max(0, maxDistance)
    rawX.set(Math.max(-limit, Math.min(limit, offsetX)))
    rawY.set(Math.max(-limit, Math.min(limit, offsetY)))
  }

  const handlePointerLeave = (event: React.PointerEvent<HTMLDivElement>) => {
    onPointerLeave?.(event)
    reset()
  }

  const handlePointerCancel = (event: React.PointerEvent<HTMLDivElement>) => {
    onPointerCancel?.(event)
    reset()
  }

  return (
    <motion.div
      data-slot="magnetic"
      {...props}
      className={cn("inline-flex", className)}
      style={{
        ...style,
        x: disabled || reduceMotion ? 0 : x,
        y: disabled || reduceMotion ? 0 : y,
      }}
      onPointerMove={handlePointerMove}
      onPointerLeave={handlePointerLeave}
      onPointerCancel={handlePointerCancel}
    >
      {children}
    </motion.div>
  )
}

export { Magnetic }

属性 Props

Magnetic 接受以下物理吸附参数配置,并继承 motion.div 的全部 HTML 属性:

属性类型默认值说明
childrenReact.ReactNode—受磁吸影响的子元素(如按钮、图标、浮动徽标等)。
strengthnumber0.18磁吸跟随强度系数(0~1)。数值越大吸附越灵敏,推荐在 0.15~0.3 之间。
maxDistancenumber12X / Y 轴方向上的最大允许偏移距离(像素 px)。
springConfigSpringOptions{ stiffness: 260, damping: 22, mass: 0.35 }Motion 物理弹簧参数配置对象(刚度、阻尼与质量)。
disabledbooleanfalse是否禁用磁吸效果。禁用时元素归零静止且不响应指针位移。
classNamestring—应用于磁吸外层容器的额外 CSS 类名(默认包含 inline-flex)。

事件 Events

Magnetic 透传底层的指针跟踪事件:

属性类型默认值说明
onPointerMove(event: React.PointerEvent<HTMLDivElement>) => void—指针在元素内移动时触发,内部通过 MotionValue 计算坐标偏移并驱动 Spring 弹簧。
onPointerLeave(event: React.PointerEvent<HTMLDivElement>) => void—指针离开元素边界时触发,内部自动平滑回弹归零。
onPointerCancel(event: React.PointerEvent<HTMLDivElement>) => void—指针事件被系统中断时触发,自动回弹归零。

使用场景与设计规范

Magnetic 能够赋予静态按钮生动的物理反馈,极大地提升用户的点击意愿。

  • 使用场景:
    • 社交媒体与快捷操作浮动栏(Dock):导航坞、工具条中的图标吸附反馈。
    • 首屏主操作 CTA(Hero CTA):官网头部的核心操作按钮。
    • 创意交互与展示项:作品展示、浮动联系小球(Floating Action Button)。
  • 设计规范与克制原则:
    • 位移幅度控制:maxDistance 建议设置在 8px~16px 以内。过大的位移会造成光标“追逐”按钮的糟糕体验,导致用户难以精准点击。
    • 移动端与触控:触控设备(Touch)由于没有持续光标位置,组件内部会自动忽略位移计算,防止页面滚动卡顿。
  • 与 Button 组合:
    • 内部子组件通常搭配 Button 或可点击的链接,磁吸容器不会影响原生点击事件触发。

场景示例

浮动社交操作栏(Dock)

在悬浮操作栏中将每个图标单独赋予微磁吸动效:

Loading…

首屏核心行动点(Hero CTA)

在官网顶部主视觉区域为操作按钮配置磁吸反馈:

Loading…

无障碍与交互 Accessibility

  • 减少动态效果降级:内置 useReducedMotion 监听。当用户系统设置了“减少动态效果(prefers-reduced-motion)”时,磁吸位移自动清零并停止监听,保证元素处于静止状态。
  • 键盘导航兼容:磁吸包装层不会干扰子元素的 Tab 焦点流与屏幕阅读器朗读,键盘用户可以正常聚焦与激活。
  • 触摸屏兼容:触摸屏上自动静止,避免干扰手势上下滑动。