How to Implement Magnetic Button Hover Physics with the Motion Library
To implement magnetic button hover physics with the Motion library, capture cursor coordinates in useMotionValue instances, derive spring-smoothed offsets with useSpring, clamp the resulting travel to a safe range via useTransform, and bind the final translations to a motion.button element while respecting the user's reduced-motion preference.
The Leonxlnx/taste-skill repository defines magnetic hover effects as a high-intensity motion pattern that must be built exclusively with Motion (the re-branded Framer Motion) and never with React useState. According to the design specification in skills/taste-skill/SKILL.md, any motion intensity above level five requires spring-based magnetic micro-physics for interactive buttons. The implementation below follows every rule in that specification, including the mandatory useReducedMotion guard for accessibility.
Motion Library Rules for Magnetic Micro-Physics
The taste-skill design system encodes these requirements in skills/taste-skill/SKILL.md. Lines 132 and 138 establish the library choice and the ban on useState for continuous data, while lines 138–139 trigger magnetic micro-physics whenever MOTION_INTENSITY exceeds five.
- Use Motion for animation. The canonical import is
import { motion } from "motion/react", as established at line 132 of the repository's skill definition. - Never track continuous values with
useState. Cursor position, magnetic pull, and other high-frequency inputs must be handled with Motion'suseMotionValue,useSpring, anduseTransformutilities. - Enable magnetic micro-physics only when
MOTION_INTENSITY > 5. Buttons are expected to pull toward the cursor using real spring physics once intensity crosses this threshold. - Respect
prefers-reduced-motion. Effects aboveMOTION_INTENSITY > 3must collapse to a static state when the user prefers reduced motion, as documented at lines 526–529.
Step-by-Step Implementation
Create a Client-Only Component
Motion values live exclusively on the client, so the component must start with a "use client" directive. In src/components/MagneticButton.tsx, place the directive at the very top before any imports to guarantee browser-side execution.
Capture Cursor Position with useMotionValue
Read the mouse position inside an onMouseMove handler and write the values directly to useMotionValue instances. This bypasses React's render cycle entirely, satisfying the rule at line 138 that continuous inputs must never be stored in useState.
The handler should calculate the vector from the button's center to the cursor. Dividing that vector by a factor such as 8 slows the pull and creates a realistic magnetic field radius without overshooting.
Apply Spring Physics and Clamp Translation
Pipe the raw offsets through useSpring to add natural elasticity. The Taste‑Skill specification favors smooth, real-world motion, so a configuration such as stiffness: 150 and damping: 25 gives the button an elastic but controlled feel.
After the spring, add a second transform with useTransform to clamp the travel distance. Limiting movement to a maximum of ±20 px prevents layout shifts and keeps the button fully visible inside its container.
Guard Against Reduced Motion
Always call useReducedMotion() at the top of the component. When this hook returns true, skip all calculations in the mouse handler and omit the x/y transforms from the style prop. The button remains static, complying with the accessibility policy defined at lines 526–529 of skills/taste-skill/SKILL.md.
Production-Ready MagneticButton Component
The following file, src/components/MagneticButton.tsx, demonstrates the complete pattern. It imports motion, useMotionValue, useSpring, useTransform, and useReducedMotion from motion/react, calculates center-relative offsets on onMouseMove, and resets the values to zero on onMouseLeave.
// src/components/MagneticButton.tsx
"use client";
import { MouseEvent } from "react";
import {
motion,
useMotionValue,
useSpring,
useTransform,
useReducedMotion,
} from "motion/react";
/**
* A button that magnetically follows the cursor.
* Works only when MOTION_INTENSITY > 5 and reduced‑motion is not requested.
*/
export function MagneticButton({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) {
const reduce = useReducedMotion(); // ① Respect reduced‑motion
const mouseX = useMotionValue(0); // ② Store raw cursor X
const mouseY = useMotionValue(0); // ③ Store raw cursor Y
// Spring‑y values that we will actually use for the translation.
const springX = useSpring(mouseX, { stiffness: 150, damping: 25 });
const springY = useSpring(mouseY, { stiffness: 150, damping: 25 });
// Convert the raw spring values to a limited offset (max ±20px).
const translateX = useTransform(springX, (v) => Math.max(-20, Math.min(20, v)));
const translateY = useTransform(springY, (v) => Math.max(-20, Math.min(20, v)));
// Mouse‑move handler – compute the vector from the button centre to the cursor.
const handleMouseMove = (e: MouseEvent<HTMLButtonElement>) => {
if (reduce) return; // skip calculations when reduced‑motion is on
const rect = e.currentTarget.getBoundingClientRect();
const offsetX = (e.clientX - (rect.left + rect.width / 2)) / 8; // ÷8 = slower pull
const offsetY = (e.clientY - (rect.top + rect.height / 2)) / 8;
mouseX.set(offsetX);
mouseY.set(offsetY);
};
// Reset to zero when the cursor leaves the button.
const handleMouseLeave = () => {
mouseX.set(0);
mouseY.set(0);
};
return (
<motion.button
className={`px-6 py-3 rounded-md bg-primary text-white ${className}`}
style={reduce ? undefined : { x: translateX, y: translateY }} // ④ Apply offset
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
{children}
</motion.button>
);
}
How the Component Satisfies the Guidelines
use client+ Motion imports guarantee client-side execution as required by the architecture.useMotionValueanduseSpringcapture continuous cursor data without triggering React re-renders, obeying the strict rule againstuseStatefor high-frequency values.useReducedMotionguard collapses the effect to a static button whenever the user prefers reduced motion.- Spring configuration (
stiffness: 150,damping: 25) delivers the real-world magnetic feel mandated by the high-intensity motion spec. - Limited translation (
±20 px) preserves layout stability by preventing the button from drifting beyond its container.
Summary
- The
Leonxlnx/taste-skillrepository requires magnetic button hover physics to be implemented exclusively with the Motion library and never with ReactuseState. - Store cursor coordinates in
useMotionValueinstances, smooth them withuseSpring, and clamp the result withuseTransformbefore applyingxandytranslations. - Always wrap the component in
"use client"because Motion values run only in the browser. - Call
useReducedMotion()to disable the effect statically for users who prefer reduced motion, as enforced byskills/taste-skill/SKILL.md.
Frequently Asked Questions
Why can't I use useState for the mouse position?
skills/taste-skill/SKILL.md at line 138 explicitly forbids tracking continuous values such as mouse coordinates with useState because every update would force a React re-render and destroy performance. Instead, useMotionValue writes values synchronously outside of React's render phase, and useSpring derives smooth animations without a single state commit.
What is the correct import path for Motion hooks?
According to the taste-skill specification at line 132, the canonical import is import { motion, useMotionValue, useSpring, useTransform, useReducedMotion } from "motion/react". This is the re-branded Framer Motion API used throughout the repository.
How do I limit how far the button travels?
Pass the spring output through useTransform to clamp the offset. In the example above, useTransform(springX, (v) => Math.max(-20, Math.min(20, v))) caps horizontal movement at ±20 px. You can adjust the 20 value to shrink or enlarge the magnetic field radius.
When should I disable the magnetic effect?
Disable the effect whenever useReducedMotion() returns true, which happens when the user's system preferences request reduced motion. The repository's accessibility rule at lines 526–529 of skills/taste-skill/SKILL.md states that any intensity above level three must fall back to a static state in this scenario.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →