Component

Magnetic

See on GitHub

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

pnpm dlx shadcn@latest add @joyco/magnetic

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'>.

PropTypeDefaultDescription
asChildbooleanfalseRender as child element via Slot
strengthnumber0.35How strongly inner content follows cursor (0–1)
easenumber350Return-to-center transition duration in ms

Inner

Extends React.ComponentPropsWithRef<'div'>.

PropTypeDefaultDescription
asChildbooleanfalseRender 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

  1. Root captures mousemove events and stores the cursor position in a ref (no re-renders).
  2. Inner attaches native mouseenter/mouseleave listeners on the Root DOM node.
  3. On hover, a requestAnimationFrame loop interpolates the inner element's translate toward the cursor offset multiplied by strength.
  4. On leave, the RAF loop stops and a CSS transition eases the element back to translate(0, 0).

Related Components

Maintainers
Downloads
0Total
0 downloads today