Skip to content
Suluu

MagnetPull

A button that reaches for the cursor before the cursor reaches it. The pull grows from exactly zero at the edge of the field, and the label travels further than the surface so the two layers read as depth.

Preview

Move your cursor near the button

Subtle

A short field for quieter surfaces.

Expressive

A wider reach for a primary action.

Installation

Registry — recommended

Copies the component into your configured shadcn UI directory and installs Motion.

npx shadcn@latest add https://suluu.site/r/magnet-pull.json

npm package

For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.

pnpm add suluu motion

Usage

MagnetPull is a native button. Everything you would pass to one — click handlers, form attributes, ARIA — works unchanged.

Registry
import { MagnetPull } from "@/components/ui/magnet-pull"

export function Hero() {
  return (
    <MagnetPull onClick={() => startTrial()}>
      Get started
    </MagnetPull>
  )
}
npm
// app/globals.css
@import "suluu/styles.css";
@source "../node_modules/suluu/dist";

// component.tsx
import { MagnetPull } from "suluu/magnet-pull"

export function Hero() {
  return <MagnetPull onClick={() => startTrial()}>Get started</MagnetPull>
}

Tuning the field

radius sets how far the field reaches and strength sets how far the button travels. Keep the radius comfortably smaller than the gap to neighboring controls, so two magnets never overlap.

Tuning
// A wide, slow, unmistakable magnet for a hero CTA.
<MagnetPull
  contentStrength={0.6}
  motionIntensity="expressive"
  radius={200}
  strength={0.4}
>
  Get started
</MagnetPull>

// A tight, barely-there magnet for dense toolbars.
<MagnetPull
  contentStrength={0.18}
  motionIntensity="subtle"
  radius={48}
  strength={0.1}
>
  Save
</MagnetPull>

In context

Use the magnetic pull sparingly on the one action that carries a page forward.

Built for small teams

Turn the next idea into something real.

A focused workspace for shipping thoughtful product work together.

Props

MagnetPull also accepts native button attributes and forwards its ref to the button element.

PropTypeDefaultDescription
childrenReactNodeContent rendered in the parallax layer.
radiusnumber120Pixels beyond the button bounds where the pull engages.
strengthnumber0.2Fraction of the cursor offset the surface travels.
contentStrengthnumber0.32Total fraction the content travels, creating the parallax.
motionIntensity"subtle" | "default" | "expressive""default"Spring character of the pull and the release.
onEngagedChange(engaged) => voidRuns when the cursor enters or leaves the field.
disabledbooleanfalseDisables the button and the magnetism.
classNamestringClass name applied to the button.
type"button" | "submit" | "reset""button"Native button type.

Theming

Override these variables in your light and dark theme scopes. The registry installs the defaults automatically.

--suluu-magnet-backgroundButton surface.
--suluu-magnet-foregroundLabel and icon color.
--suluu-magnet-hoverSurface while hovered.
--suluu-magnet-ringKeyboard focus ring.
--suluu-magnet-offsetColor behind the focus ring offset.
--suluu-magnet-shadowResting depth under the button.

The button also exposes data-magnet-engaged, so you can style the engaged state directly with data-[magnet-engaged=true]:shadow-lg.

Accessibility

MagnetPull renders a plain native button and adds no ARIA of its own. Keyboard users get a focus-visible ring and deliberately no magnetism — there is no cursor to follow, and a control that moves when you tab to it is disorienting.

The effect only runs on devices matching (hover: hover) and (pointer: fine), so touch devices never see it. Under prefers-reduced-motion the pointer listener is never attached at all — the button is genuinely inert rather than animated at zero duration — while staying fully clickable.

↑↓ to navigate↵ to openesc to close