Skip to content
Go back

Magnetic buttons and cursor follow: one formula, four outputs

约 4 分钟
实验瓶

Magnetic buttons look like a trick until you pull them apart. The math is one line: multiply the distance between the pointer and the element center by a strength factor. The rest is choosing where that offset goes.

The demo below is zero-dependency. Move the pointer near the buttons and cards to see four outputs. Touch devices turn off motion and the custom cursor automatically.

Move the pointer near the buttons or cards. Touch devices fall back automatically.Open the demo in a new page

The core claim is short:

offset = (pointer - center) × strength, then ease it with lerp.

Note

Magnets belong on large primary CTAs only. Keep strength modest and respect prefers-reduced-motion. Do not use them on nav or form submit buttons.

Magnetic button

Three decisions matter:

The hit area should be larger than the button. If you wait for mouseenter, the button jumps. Expand the rect by 90px so the button starts moving before the pointer touches it.

Lerp is required. Assigning the offset directly sticks the button to the cursor. A damping of 0.15 is what creates the pull.

Use one requestAnimationFrame loop. Do not bind mousemove on every button. Record the pointer once, then update everything in a shared loop.

// public/demos/magnetic-cursor.html
const lerp = (a, b, t) => a + (b - a) * t;
const RADIUS = 90;

const inside =
  Math.abs(dx) < r.width / 2 + RADIUS &&
  Math.abs(dy) < r.height / 2 + RADIUS;

const tx = inside ? dx * m.strength : 0;
const ty = inside ? dy * m.strength : 0;
m.x = lerp(m.x, tx, 0.15);
m.y = lerp(m.y, ty, 0.15);
m.el.style.transform = `translate3d(${m.x}px, ${m.y}px, 0)`;

When the pointer leaves the expanded area, set the target to 0 and let lerp slide back.

Dual-layer magnet

Give inner text and icons an extra strength so they travel a little farther. That small parallax makes the button feel thicker:

<a class="btn magnet" data-strength="0.30">
  <span class="label" data-inner="0.20">View work</span>
  <span class="arrow" data-inner="0.45"></span>
</a>

The arrow moves faster than the label, so direction reads more clearly.

Spotlight follow

This variant never moves the DOM. It only writes two CSS variables and lets radial-gradient render:

.glow::before {
  pointer-events: none;
  background: radial-gradient(
    260px circle at var(--mx) var(--my),
    rgba(94, 159, 232, 0.45),
    transparent 72%
  );
}
glow.style.setProperty("--mx", mx - r.left + "px");
glow.style.setProperty("--my", my - r.top + "px");

The pseudo-element must use pointer-events: none, or it will block clicks.

3D tilt

Normalize pointer position to -1 to 1, then multiply by a max angle. Invert the X axis so moving the pointer down tilts the card away from you.

const nx = clamp((mx - cx) / (r.width / 2), -1, 1);
const ny = clamp((my - cy) / (r.height / 2), -1, 1);
const targetRotY = nx * 14;
const targetRotX = -ny * 14;

The parent needs perspective, or the rotation looks flat. 700px is a steady value: below 500px it warps, and above 1200px the depth almost disappears. Lift the content with translateZ(28px).

Custom cursor

Two elements and two lerp rates create the trail:

dotX = lerp(dotX, mx, 0.40);  // nearly sticks to the hand
ringX = lerp(ringX, mx, 0.14); // lags enough to trail

When you hide the system cursor, gate it with media queries, or touch devices become painful:

@media (hover: hover) and (pointer: fine) {
  body { cursor: none; }
}
@media (hover: none) {
  .cursor-dot,
  .cursor-ring { display: none; }
}

Knobs worth changing

ParameterCurrent valueNotes
Magnet strength0.30 to 0.35Above 0.5 feels wild
Hit radius90pxSmall buttons can use 120
Magnet damping0.15Smaller is slower
Inner extras0.20 / 0.45Larger gap means more depth
Max tilt14degAbove 20deg looks wrong
Ring damping0.14Smaller means a longer trail

Open the complete demo in a new page. Previous Frontend Lab post: Scroll storytelling.


Share this post:

接着读

  1. Running 24,000 particles with raw WebGLGenerate the initial particle data once, then calculate each frame's positions in a vertex shader. The complete interactive demo runs inside the post.同属 前端实验室 · 同系列第 4 / 4 篇
  2. What changed in the MCP draft: sessions and handshake are goneNotes from the official MCP spec draft's changelog, compared against the 2025-11-25 version, focused on sessions, the handshake, MRTR, and migration impact.
  3. Latest digestWhat I've been reading lately.

Discussion

评论

用 GitHub 账号登录即可留言。想法、纠错、补充都欢迎。