Skip to main content

Building a Magnetic Cursor Effect That Actually Feels Good

Learn how to create smooth, physics-based magnetic cursor interactions with Next.js, TypeScript, and GSAP.

Magnetic cursor

As a front-end developer, I've been pretty obsessed with cursor interactions lately. So I tend to hover over every button, link, and interactive element on websites just to see what happens. Then, late last year, seeing all these luxury brand sites with their buttery-smooth magnetic cursors set me off thinking even deeper about physics-based interactions. So I've been watching, pondering, and exploring: How do we create cursor effects that feel natural? What makes a magnetic interaction satisfying versus frustrating? Today, I want to discuss building a magnetic cursor that actually feels right. Hope you're ready to go on a wild ride with me!

Remember When Custom Cursors Were Just... Cursors?

For a period in the early 2000s, custom cursors meant replacing your pointer with a sparkly wand or a dragon (don't pretend you didn't have one). We've come a long way since then. Modern cursor interactions aren't just about changing the appearance — they're about creating physical relationships between the cursor and interface elements.

You know those websites where the cursor gently pulls toward buttons? Where cards seem to reach out as you approach them? That's what we're building today. And we're doing it with Next.js 15, TypeScript, and GSAP because, well, gotta use the right tools for the job.

The Physics of Attraction (No, Not That Kind)

Let's talk about what makes a magnetic effect feel natural. Real magnets don't just snap things toward them — there's a gradual increase in force as objects get closer. That's the key insight that makes or breaks these interactions.

Here's the basic physics we need to understand:

Simple, right? As distance approaches zero, force approaches one. As distance reaches the threshold, force fades to zero. It's like gravity, but for buttons.

Setting Up Our Magnetic Universe

First, we need to create a custom cursor that can be influenced by magnetic fields. Think of it as a particle floating in space, ready to be pulled by nearby objects.

We're tracking two things here: where the mouse actually is, and where our custom cursor is displayed. The magic happens in the gap between them.

The Animation Loop That Makes It Smooth

Here's where things get interesting. We can't just snap the cursor to positions — that would feel jarring. Instead, we use linear interpolation (lerp for the cool kids) to smoothly transition between positions:

That 0.15 factor? That's our "smoothness dial." Lower values create more lag and fluidity. Higher values make it snappier. After testing with dozens of users (okay, my friends), 0.15 hits the sweet spot.

Creating Magnetic Elements (The Fun Part)

Now for the actual magnetic effect. We need a wrapper component that creates a magnetic field around any element:

See that multiplier of 20? That's not arbitrary. Through extensive testing (moving my mouse around for hours), I found that raw force values are too subtle. The multiplier makes the effect visible without being obnoxious.

The Edge-Only Variant (For Those Big Beautiful Cards)

Here's the thing about magnetic effects on large elements: if the entire card is magnetic, it feels... wrong. Like the card is floating on jello. Not good.

The solution? Edge-only magnetism:

This creates a subtle "reaching out" effect when you approach the edges of cards. It's like the card is saying "hey, interact with me" without screaming it.

Cursor State Management (Because Context is Everything)

Different elements need different cursor behaviors. A video might want a play button cursor. Cards might want the cursor to grow. Buttons might want extra magnetic strength.

Enter the cursor context:

Now any component can communicate with the cursor:

And the cursor responds accordingly:

The Scroll Problem Nobody Talks About

Here's a fun bug I discovered: scroll the page without moving your mouse, and suddenly your magnetic effects don't work when elements scroll under the cursor. Why? Because mousemove doesn't fire on scroll.

The fix is elegant:

Now the magnetic effect updates even during passive scrolling. Your users won't even know this was a problem, but they'll feel that everything "just works."

Performance Considerations (Because 60fps Matters)

Let's talk about the elephant in the room: performance. Running physics calculations on every mousemove event can get expensive. Here's how we keep things smooth:

1. Use RequestAnimationFrame for the Main Loop

The cursor position updates happen in a RAF loop, not on every mouse event. This ensures we never exceed 60fps and the browser can optimize rendering.

2. Bounds Caching

We're not calling getBoundingClientRect() on every mouse move — that would tank performance.

3. Early Exits

No point doing math we don't need.

The Strength Values That Actually Work

After implementing this on a luxury homepage (because where else would you put a magnetic cursor?), here are the strength values that feel right:

These aren't random. I started with strength at 0.7 (way too strong — buttons were flying around like they were possessed). Then 0.35 (still too much). Then 0.05-0.18 range, and suddenly everything felt... right.

Handling Touch Devices (Spoiler: Don't)

Here's a pro tip that took me embarrassingly long to figure out: detect touch devices and bail out early.

There's no point rendering a custom cursor that follows... nothing. Touch devices don't have cursors. Save those CPU cycles.

The Hydration Gotcha

If you're using Next.js (or any SSR framework), you'll hit this fun error:

Warning: Prop `style` did not match. Server: null Client: "transform: translate3d(..."

The fix? Delay cursor initialization until after hydration:

The cursor doesn't need to be server-rendered anyway. It's purely a client-side enhancement.

Making It Your Own

The beauty of this system is its flexibility. Want a different easing function? Change it:

Want the cursor to change color on different sections? Add a color prop to your context. Want it to leave a trail? Add multiple cursor elements with staggered animations.

The point is: once you have the physics foundation, you can build whatever creative interactions you want on top.

What We've Built

Let's recap what we've created:

  • A custom cursor that smoothly follows mouse movement
  • Magnetic attraction between cursor and elements
  • Edge-only magnetism for large elements  
  • Multiple cursor states (default, hover, play)
  • Scroll-aware magnetic updates
  • Performance-optimized animation loop
  • Touch device detection
  • SSR compatibility

Not too shabby for ~800 lines of code.

The Little Details That Matter

You know what separates good magnetic cursors from great ones? The details nobody notices (until they're wrong):

  • Cursor returns to normal when leaving magnetic fields - with a smooth transition, not a snap
  • Magnetic strength increases gradually - no sudden jumps
  • Elements scale slightly when pulled - just 2% is enough to sell the effect
  • Cleanup on unmount - no memory leaks from orphaned event listeners
  • Resize handling - magnetic fields update when viewport changes

These aren't features users request. They're the things that make interactions feel polished versus janky.

Where to Go From Here

This magnetic cursor is just the beginning. You could:

  • Add inertia to the cursor movement for even more realistic physics
  • Create repelling effects for certain elements
  • Build cursor trails that react to velocity
  • Implement different magnetic behaviors for different element types
  • Add sound effects (okay maybe don't do this one)

The foundation we've built is solid and extensible. The physics are real(ish). The performance is optimized. The edge cases are handled.

Final Thoughts

Building a magnetic cursor taught me something important: users don't care about the physics equations or the optimization tricks. They care about how it feels. And making something feel right takes iteration, testing, and a willingness to throw away code that technically works but doesn't feel good.

That strength value I mentioned going from 0.7 to 0.05? That wasn't a failure — that was learning what "subtle" actually means in practice. Those hours spent debugging the scroll issue? That's what makes the difference between a demo and production-ready code.

So go ahead, implement your own magnetic cursor. Start with the code here, but don't stop there. Tweak the values. Try different easing functions. Break things and fix them. Because that's how you go from following tutorials to creating experiences that make people say "whoa, how'd they do that?"

Happy coding! And remember: with great magnetic power comes great responsibility to not overdo it.

About the Author

Dominique Degottex

I'm a product designer that can code.

15+ years pushing the boundaries between UX design and development. I transform complex ideas into stunning digital experiences – from wireframes to production-ready code. Currently crafting web solutions from Bangkok for clients worldwide.

Related Posts