Browse the library
Components / Buttons
Magnetic button
FreeButton that pulls toward the cursor with spring physics while its label drifts slightly further for depth.
View as Markdownmotion
components/facade/magnetic-button.tsx
"use client";
import * as React from "react";
import { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "motion/react";
import { cn } from "@/lib/utils";
export type MagneticButtonVariant = "brand" | "solid" | "outline";
export interface MagneticButtonProps
extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
"onDrag" | "onDragStart" | "onDragEnd" | "onAnimationStart" | "onAnimationEnd"
> {
/** How strongly the shell follows the cursor, 0 to 1. Defaults to 0.35. */
strength?: number;
/** Extra multiplier applied to the inner label. Defaults to 1.6. */
labelStrength?: number;
/** Distance in px outside the button where the pull starts. Defaults to 40. */
radius?: number;
variant?: MagneticButtonVariant;
/** Pass `href` to render an `<a>` instead of a `<button>`. */
href?: string;
className?: string;
children?: React.ReactNode;
}
const variants: Record<MagneticButtonVariant, string> = {
brand:
"bg-brand text-brand-foreground shadow-[0_12px_32px_-12px_color-mix(in_oklch,var(--brand)_80%,transparent),inset_0_1px_0_0_color-mix(in_oklch,var(--brand-foreground)_25%,transparent)]",
solid:
"bg-primary text-primary-foreground shadow-[0_12px_28px_-14px_color-mix(in_oklch,var(--foreground)_55%,transparent),inset_0_1px_0_0_color-mix(in_oklch,var(--primary-foreground)_15%,transparent)]",
outline: "border border-border bg-background text-foreground hover:bg-muted/60",
};
const spring = { stiffness: 320, damping: 22, mass: 0.6 };
export function MagneticButton({
strength = 0.35,
labelStrength = 1.6,
radius = 40,
variant = "brand",
href,
className,
children,
type = "button",
...rest
}: MagneticButtonProps) {
const reduce = useReducedMotion();
const zoneRef = React.useRef<HTMLSpanElement>(null);
const x = useMotionValue(0);
const y = useMotionValue(0);
const sx = useSpring(x, spring);
const sy = useSpring(y, spring);
const lx = useTransform(sx, (v) => v * labelStrength);
const ly = useTransform(sy, (v) => v * labelStrength);
const handleMove = (e: React.PointerEvent<HTMLSpanElement>) => {
if (reduce || e.pointerType === "touch") return;
const el = zoneRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
x.set((e.clientX - (rect.left + rect.width / 2)) * strength);
y.set((e.clientY - (rect.top + rect.height / 2)) * strength);
};
const reset = () => {
x.set(0);
y.set(0);
};
const shellClass = cn(
"relative z-[1] inline-flex h-12 items-center justify-center rounded-full px-7 text-[15px] font-medium tracking-[-0.01em] will-change-transform",
"transition-[background-color,box-shadow] duration-300",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"disabled:pointer-events-none disabled:opacity-50",
variants[variant],
className,
);
const label = (
<motion.span style={{ x: lx, y: ly }} className="relative inline-flex items-center gap-2 [&_svg]:size-4">
{children}
</motion.span>
);
return (
<span
ref={zoneRef}
onPointerMove={handleMove}
onPointerLeave={reset}
className="relative inline-flex"
style={{ padding: radius, margin: -radius }}
>
{href !== undefined ? (
<motion.a
href={href}
style={{ x: sx, y: sy }}
className={shellClass}
onBlur={reset}
{...(rest as Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "onDrag" | "onDragStart" | "onDragEnd" | "onAnimationStart" | "onAnimationEnd">)}
>
{label}
</motion.a>
) : (
<motion.button
type={type}
style={{ x: sx, y: sy }}
className={shellClass}
whileTap={reduce ? undefined : { scale: 0.97 }}
onBlur={reset}
{...rest}
>
{label}
</motion.button>
)}
</span>
);
}
export default MagneticButton;app/page.tsx
import { ArrowUpRight, Calendar } from "lucide-react";
import { MagneticButton } from "@/components/facade/magnetic-button";
export default function Demo() {
return (
<div className="relative w-full max-w-2xl overflow-hidden rounded-2xl border border-border bg-card px-6 py-14 text-center text-card-foreground sm:px-12">
<div
aria-hidden
className="pointer-events-none absolute inset-0 [background-image:radial-gradient(circle_at_1px_1px,color-mix(in_oklch,var(--foreground)_10%,transparent)_1px,transparent_0)] [background-size:22px_22px] [mask-image:radial-gradient(ellipse_at_center,black,transparent_70%)]"
/>
<div className="relative">
<p className="font-mono text-xs uppercase tracking-[0.18em] text-muted-foreground">Studio Juniper, Portland</p>
<h3 className="mx-auto mt-4 max-w-md font-display text-3xl font-semibold tracking-tight sm:text-4xl">
Have a space that deserves better light?
</h3>
<p className="mx-auto mt-3 max-w-sm text-[15px] leading-relaxed text-muted-foreground">
Interior design for homes and boutique hospitality. Taking four new projects this fall.
</p>
<div className="mt-9 flex flex-wrap items-center justify-center gap-4">
<MagneticButton>
Start a project
<ArrowUpRight aria-hidden />
</MagneticButton>
<MagneticButton variant="outline">
<Calendar aria-hidden />
Book a 20 min call
</MagneticButton>
</div>
<p className="mt-8 text-xs text-muted-foreground">Move your cursor near the buttons</p>
</div>
</div>
);
}Installation
$
npx shadcn@latest add https://facadeui.com/r/magnetic-button.jsonUsage
import { MagneticButton } from "@/components/facade/magnetic-button";
export default function Page() {
return <MagneticButton />;
}Props
MagneticButtonProps is exported, so you can extend or wrap it. Every prop has a default; <MagneticButton /> renders the example above.
| Prop | Type | Description |
|---|---|---|
| strength | number | How strongly the shell follows the cursor, 0 to 1. Defaults to 0.35. |
| labelStrength | number | Extra multiplier applied to the inner label. Defaults to 1.6. |
| radius | number | Distance in px outside the button where the pull starts. Defaults to 40. |
| variant | MagneticButtonVariant | |
| href | string | Pass `href` to render an `<a>` instead of a `<button>`. |
| className | string | |
| children | React.ReactNode |
Ask your agent
With the Facade MCP server connected, this prompt is enough:
Add the Facade magnetic button to this page and adapt the copy to our business.