返回动效库
指针进阶

磁吸悬浮

鼠标靠近目标元素时,元素会像被磁铁吸引一样向鼠标指针方向产生弹性位移。

悬停时continuous

交互预览

动效定义与代码

{
  "slug": "magnetic-hover",
  "name": "磁吸悬浮",
  "nameEn": "Magnetic Hover",
  "description": "鼠标靠近目标元素时,元素会像被磁铁吸引一样向鼠标指针方向产生弹性位移。",
  "descriptionEn": "Element attracts towards the mouse cursor like a magnet with a spring physics effect.",
  "category": "micro-interaction",
  "tags": [
    "hover",
    "magnetic",
    "physics",
    "interactive"
  ],
  "trigger": "on-hover",
  "difficulty": "intermediate",
  "duration": "continuous",
  "easing": "spring",
  "cssProperties": [
    "transform"
  ],
  "isGPUAccelerated": true,
  "previewBg": "light",
  "keywords": [
    "magnetic",
    "spring",
    "button hover",
    "physics",
    "attract"
  ],
  "useCases": [
    "Navigation bar icons",
    "Floating Action Buttons (FAB)",
    "Call-to-action buttons"
  ],
  "relatedAnimations": [
    "hover-lift"
  ],
  "recommendedStyles": [
    "minimalist-flat",
    "apple-style",
    "neo-brutalist"
  ],
  "codeSnippets": [
    {
      "label": "Tailwind CSS + React",
      "language": "tsx",
      "code": "import { useRef, useState } from \"react\";\n\nexport function MagneticButton({ children }: { children: React.ReactNode }) {\n  const ref = useRef<HTMLButtonElement>(null);\n  const [transform, setTransform] = useState(\"\");\n\n  const handleMouse = (e: React.MouseEvent<HTMLButtonElement>) => {\n    const { clientX, clientY } = e;\n    const { height, width, left, top } = ref.current!.getBoundingClientRect();\n    const x = (clientX - (left + width / 2)) * 0.3;\n    const y = (clientY - (top + height / 2)) * 0.3;\n    setTransform(`translate(${x}px, ${y}px)`);\n  };\n\n  const reset = () => setTransform(\"\");\n\n  return (\n    <button\n      ref={ref}\n      onMouseMove={handleMouse}\n      onMouseLeave={reset}\n      className=\"px-6 py-3 bg-foreground text-background transition-transform duration-200 ease-out will-change-transform\"\n      style={{ transform }}\n    >\n      {children}\n    </button>\n  );\n}"
    },
    {
      "label": "AnimeJS",
      "language": "tsx",
      "code": "\"use client\";\n\nimport { useEffect, useRef, type MouseEvent, type ReactNode } from \"react\";\n\ntype AnimeModule = typeof import(\"animejs\");\ntype AnimeAnimation = {\n  cancel(): unknown;\n  revert(): unknown;\n};\n\nfunction prefersReducedMotion() {\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\nexport function MagneticButton({ children }: { children: ReactNode }) {\n  const ref = useRef<HTMLButtonElement>(null);\n  const animeRef = useRef<Promise<AnimeModule> | null>(null);\n  const animationRef = useRef<AnimeAnimation | null>(null);\n\n  useEffect(() => {\n    return () => {\n      animationRef.current?.revert();\n    };\n  }, []);\n\n  function getAnime() {\n    animeRef.current ??= import(\"animejs\");\n    return animeRef.current;\n  }\n\n  async function moveTo(x: number, y: number, duration = 180) {\n    const element = ref.current;\n    if (!element || prefersReducedMotion()) return;\n\n    const { animate } = await getAnime();\n    if (ref.current !== element) return;\n\n    animationRef.current?.cancel();\n    animationRef.current = animate(element, {\n      x,\n      y,\n      duration,\n      ease: \"out(3)\",\n    });\n  }\n\n  function handleMouseMove(event: MouseEvent<HTMLButtonElement>) {\n    const element = ref.current;\n    if (!element) return;\n\n    const { clientX, clientY } = event;\n    const { height, width, left, top } = element.getBoundingClientRect();\n    const x = (clientX - (left + width / 2)) * 0.3;\n    const y = (clientY - (top + height / 2)) * 0.3;\n    void moveTo(x, y);\n  }\n\n  return (\n    <button\n      ref={ref}\n      onMouseMove={handleMouseMove}\n      onMouseLeave={() => void moveTo(0, 0, 420)}\n      className=\"px-6 py-3 bg-foreground text-background will-change-transform\"\n    >\n      {children}\n    </button>\n  );\n}"
    },
    {
      "label": "Framer Motion",
      "language": "tsx",
      "code": "import { useRef, useState } from \"react\";\nimport { motion } from \"framer-motion\";\n\n// Tip: consider disabling for reduced motion users\n// import { useReducedMotion } from \"framer-motion\";\n\nexport function MagneticButton({ children }: { children: React.ReactNode }) {\n  const ref = useRef<HTMLButtonElement>(null);\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n\n  const handleMouse = (e: React.MouseEvent<HTMLButtonElement>) => {\n    const { clientX, clientY } = e;\n    const { height, width, left, top } = ref.current!.getBoundingClientRect();\n    const middleX = clientX - (left + width / 2);\n    const middleY = clientY - (top + height / 2);\n    setPosition({ x: middleX * 0.3, y: middleY * 0.3 });\n  };\n\n  const reset = () => setPosition({ x: 0, y: 0 });\n\n  return (\n    <motion.button\n      ref={ref}\n      onMouseMove={handleMouse}\n      onMouseLeave={reset}\n      animate={{ x: position.x, y: position.y }}\n      transition={{ type: \"spring\", stiffness: 150, damping: 15, mass: 0.1 }}\n      className=\"px-6 py-3 bg-foreground text-background\"\n    >\n      {children}\n    </motion.button>\n  );\n}"
    }
  ]
}