Magnetic button

Free · MIT

Button and its label are pulled toward a nearby cursor with a smooth falloff and spring back on leave; off for touch and reduced motion.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/magnetic-button.json

The file lands in components/goodui/magnetic-button.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/magnetic-button works too.

Usage

import { ArrowRight } from "lucide-react"
import { MagneticButton } from "@/components/goodui/magnetic-button"

export function FooterCta({ onStart }: { onStart: () => void }) {
  return (
    <MagneticButton size="lg" onClick={onStart}>
      Get started free
      <ArrowRight className="rtl:rotate-180" />
    </MagneticButton>
  )
}

When to use

Use it for
  • A hero CTA or footer "Get started" on a marketing site where a playful, tactile hover fits the brand.
  • Round icon buttons (social links, a floating chat button) that should feel physical under the mouse.
  • Portfolio and agency pages where pointer interactions are part of the design.
Reach for something else
  • Dense app UI (tables, toolbars, forms): a moving target slows precise clicking. Use button.
  • Touch-first screens: the effect is off on touch and coarse pointers, so it adds nothing there. Use button or ripple-button.
  • A hover lift without pointer tracking: button with animation="lift" is simpler.
If you needUse
Subtle pull for an app-like CTAstrength={0.2} labelStrength={0.1}
Strong, playful pullstrength={0.5} labelStrength={0.3} radius={120}
Round icon buttonsize="icon" aria-label="…"
Only react when the cursor is on the buttonradius={0}

Examples

Round icon button with a stronger pull

<MagneticButton size="icon" variant="outline" strength={0.5} radius={100} aria-label="Chat with support">
  <MessageCircle />
</MagneticButton>

API reference

PropTypeDefault
variant
"default" | "secondary" | "outline" | "ghost", matching shadcn's button variants.
"outline" | "default" | "ghost" | "secondary""default"
size
"sm" | "md" | "lg" | "icon"; "icon" is a round 44px button.
"sm" | "md" | "lg" | "icon""md"
strength
Share of the pointer offset the button follows (0–1).
number0.35
labelStrength
Extra share for the label, for a parallax feel.
number0.2
radius
Distance in px outside the button where the pull starts.
number70

Also accepts every <button> attribute.

Accessibility

Enter / SpaceActivates the button (native <button>).
TabMoves focus; a focus ring shows on keyboard focus only. Keyboard focus does not move the button.
  • Renders a native <button> (motion.button) inside an inline-flex <span> wrapper that is used to measure the resting position.
  • The effect only runs for a fine pointer that can hover (mouse, trackpad); touch input and keyboard users get a normal static button.
  • Disabled buttons do not move.

Motion

  • The button moves toward the pointer by `strength` (0.35) of the offset and the label by `labelStrength` (0.2), both on a spring (stiffness 220, damping 15, mass 0.6).
  • The pull is full over the button and eases to zero at `radius` px (70) outside it, using a smoothstep curve; leaving springs it back to the center.
  • Tap scales to 0.95.
  • Listening stops while the button is off-screen, while the tab is hidden and when the pointer is not fine.
  • Reduced motion: no pull and no tap scale.
  • Uses motion values and springs, so pointer moves do not re-render React. The window pointermove listener is only attached while the button is visible.

Notes for AI

Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.

  • variant values are default, secondary, outline, ghost; sizes sm, md, lg, icon.
  • strength and labelStrength are fractions of the pointer offset (0 to 1), not pixels; radius is in px.
  • className goes on the button; the wrapping <span> is internal, so use a parent element for layout spacing.
  • Pass icons and text as children; they move together as the label.

FAQ

Does it work on phones?

It renders as a normal button. The pull only runs for a fine pointer that can hover (mouse or trackpad).

Can I use it on several buttons?

Yes. Each button only listens while it is visible, but keep it to a few buttons so the page does not feel jumpy.

Related