Magnetic
A cursor-attraction wrapper that makes inner content subtly follow the mouse position.
'use client'
import { Logo } from '@/components/logos'
import * as Magnetic from '@/components/magnetic'
function MagneticDemo() {
return (
<div className="flex min-h-40 w-full items-center justify-center gap-10 p-8">
{/* Strong pull */}
<Magnetic.Root className="inline-flex">
<Magnetic.Inner className="bg-primary text-primary-foreground inline-flex size-24 items-center justify-center rounded-full text-sm font-medium select-none">
<Logo className="size-16" />
</Magnetic.Inner>
</Magnetic.Root>
</div>
)
}
export default MagneticDemo
Installation
Usage
import * as Magnetic from '@/components/magnetic'
<Magnetic.Root>
<Magnetic.Inner>
<button>Hover me</button>
</Magnetic.Inner>
</Magnetic.Root>With asChild
Use asChild on both Root and Inner to render as the child element without a wrapper div:
<Magnetic.Root asChild>
<a href="/about">
<Magnetic.Inner asChild>
<span>About</span>
</Magnetic.Inner>
</a>
</Magnetic.Root>Custom strength and ease
<Magnetic.Root strength={0.5} ease={500}>
<Magnetic.Inner>Click</Magnetic.Inner>
</Magnetic.Root>Props
Root
Extends React.ComponentPropsWithRef<'div'>.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render as child element via Slot |
strength | number | 0.35 | How strongly inner content follows cursor (0–1) |
ease | number | 350 | Return-to-center transition duration in ms |
Inner
Extends React.ComponentPropsWithRef<'div'>.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render as child element via Slot |
Accessibility
The component respects the prefers-reduced-motion media query. When reduced motion is enabled, all magnetic transforms are disabled and the inner content remains stationary.
How It Works
- Root captures
mousemoveevents and stores the cursor position in a ref (no re-renders). - Inner attaches native
mouseenter/mouseleavelisteners on the Root DOM node. - On hover, a
requestAnimationFrameloop interpolates the inner element'stranslatetoward the cursor offset multiplied bystrength. - On leave, the RAF loop stops and a CSS transition eases the element back to
translate(0, 0).