磁吸悬浮
鼠标靠近目标元素时,元素会像被磁铁吸引一样向鼠标指针方向产生弹性位移。
交互预览
动效定义与代码
{
"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}"
}
]
}