The Ballmac Button with a magnetic pull: it drifts toward a nearby mouse pointer on a spring, and its label moves a little further for depth. Takes every Button prop.
import { ArrowRight } from "lucide-react"
import { MagneticButton } from "@/components/ballmac/magnetic-button"
export default function MagneticButtonDemo() {
return (
<div className="flex flex-wrap items-center justify-center gap-6 p-8">
<MagneticButton size="lg" shape="pill">
Start building <ArrowRight className="rtl:rotate-180"/>
</MagneticButton>
<MagneticButton size="lg" shape="pill" variant="outline" strength={0.15}>
Read the docs
</MagneticButton>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/magnetic-buttonInstall the dependencies.
$ pnpm add motion@^12Add the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/button @ballmac/motion-presetsCopy the source into your project.
components/ballmac/magnetic-button.tsx// Ballmac UI: Magnetic Button. https://ui.ballmac.com/components/magnetic-button "use client" import * as React from "react" import { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "motion/react" import { Button, type ButtonProps } from "@/components/ballmac/button" import { cn } from "@/lib/utils" import { spring } from "@/lib/ballmac/motion" type MagneticButtonProps = Omit<ButtonProps, "asChild"> & { /** How far the button follows the pointer, as a fraction of the pointer's offset from its center. */ strength?: number /** Distance in pixels outside the button's edge where the pull starts. */ radius?: number /** Classes for the wrapper that holds the button's place in the layout. */ wrapperClassName?: string } /** The label travels this much further than the button, which reads as depth. */ const LABEL_DEPTH = 0.35 function MagneticButton({ strength = 0.2, radius = 72, wrapperClassName, disabled, loading, children, ...props }: MagneticButtonProps) { const anchorRef = React.useRef<HTMLSpanElement>(null) const reduceMotion = useReducedMotion() const x = useMotionValue(0) const y = useMotionValue(0) const springX = useSpring(x, spring.snappy) const springY = useSpring(y, spring.snappy) const labelX = useTransform(springX, (v) => v * LABEL_DEPTH) const labelY = useTransform(springY, (v) => v * LABEL_DEPTH) const inactive = !!reduceMotion || !!disabled || !!loading React.useEffect(() => { if (inactive) { x.set(0) y.set(0) return } const anchor = anchorRef.current if (!anchor) return const onMove = (event: PointerEvent) => { if (event.pointerType !== "mouse") return // The anchor is never transformed, so its box is the button's resting position. const rect = anchor.getBoundingClientRect() const dx = event.clientX - (rect.left + rect.width / 2) const dy = event.clientY - (rect.top + rect.height / 2) // Distance from the button's edge (0 when the pointer is over it). const outsideX = Math.max(0, Math.abs(dx) - rect.width / 2) const outsideY = Math.max(0, Math.abs(dy) - rect.height / 2) const distance = Math.hypot(outsideX, outsideY) if (distance > radius) { x.set(0) y.set(0) return } const falloff = radius > 0 ? 1 - distance / radius : 1 x.set(dx * strength * falloff) y.set(dy * strength * falloff) } const reset = () => { x.set(0) y.set(0) } // Pointer left the window: relatedTarget is null. const onOut = (event: PointerEvent) => { if (!event.relatedTarget) reset() } window.addEventListener("pointermove", onMove, { passive: true }) document.addEventListener("pointerout", onOut) window.addEventListener("blur", reset) return () => { window.removeEventListener("pointermove", onMove) document.removeEventListener("pointerout", onOut) window.removeEventListener("blur", reset) } }, [inactive, radius, strength, x, y]) return ( <span ref={anchorRef} data-slot="magnetic-button" className={cn("inline-flex", wrapperClassName)}> <motion.span className="inline-flex" style={{ x: springX, y: springY }}> <Button disabled={disabled} loading={loading} {...props}> <motion.span data-slot="magnetic-button-label" className="inline-flex items-center gap-[inherit]" style={{ x: labelX, y: labelY }}> {children} </motion.span> </Button> </motion.span> </span> ) } export { MagneticButton, type MagneticButtonProps }Update the import paths to match your project setup.
Usage
import { MagneticButton } from "@/components/ballmac/magnetic-button"The full example is in the Code tab above.
API reference
| Prop | Type | Default |
|---|---|---|
strengthHow far the button follows the pointer, as a fraction of the pointer's offset from its center. | number | 0.2 |
radiusDistance in pixels outside the button's edge where the pull starts. | number | 72 |
wrapperClassNameClasses for the wrapper that holds the button's place in the layout. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Enter / Space | Activates the button |
| Tab | Moves focus to the button |
Use with AI
Use like Button (variant, size, shape, loading, onClick…). The pull only reacts to a mouse, stops while disabled or loading, and is off under reduced motion, where it is a plain Button. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Magnetic Button (@ballmac/magnetic-button) to this project with the shadcn MCP, then use it where it fits.
Use it for
- The single primary call to action in a hero or pricing section
- A playful secondary action on a marketing page
Not for
- Forms, toolbars and dense UI (use button)
- Several buttons side by side; one magnetic button per view keeps it meaningful
- Rendering a link (not supported; use an <a> with buttonVariants instead)
Registry JSON: https://ui.ballmac.com/r/magnetic-button.json
Credits
Free to use in personal and commercial projects.
- npm
- motion@^12
Pairs well with
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Border Beam
A short beam of light that travels around the inside edge of its parent's border at a constant speed. Decorative, token-colored, hidden under reduced motion.
Animated Beam
An SVG beam that connects two elements with a curved path and sends a glowing gradient pulse along it. Follows layout changes, pauses off-screen, static under reduced motion.
Animated Grid
A decorative hairline grid background where a few random cells softly light up and fade, like instrument lights. SVG, hydration-safe, static under reduced motion.