Magnetic Cursor

Category: Cursor Animations. Last updated: Aug 20, 2025

Setup — External Scripts

Setup: External Scripts
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
CDN 2
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/Flip.min.js"></script>

Code

index.html
html
<!-- Cursor follower -->
<div class="cursor">
  <div class="cursor-bg"></div>
</div>

<!-- Example of a link -->
<a data-magnetic-cursor-target href="#" class="magnetic-link">
  <div class="magnetic-link__inner">
    <span class="magnetic-link__label">Here’s a link</span>
    <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 14 32" fill="none" class="magnetic-link__icon">
        <path d="M3 24L11 16L3 8" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"></path>
    </svg>
  </div>
  <div data-magnetic-cursor-bg class="magnetic-link__bg"></div>
</a>
styles.css
css
.magnetic-link {
  color: inherit;
  padding: .5em .875em;
  text-decoration: none;
  position: relative;
}

.magnetic-link__inner {
  z-index: 1;
  grid-column-gap: .35em;
  grid-row-gap: .35em;
  justify-content: center;
  align-items: center;
  display: flex;
  position: relative;
}

.magnetic-link__icon {
  justify-content: center;
  align-items: center;
  width: .4em;
  margin-bottom: -.125em;
  display: flex;
}

.magnetic-link__bg {
  z-index: 0;
  border-radius: .25em;
  position: absolute;
  inset: 0%;
}

.cursor {
  aspect-ratio: 1;
  border-radius: 100em;
  width: .75em;
  position: fixed;
  inset: 0% auto auto 0%;
}

.cursor-bg {
  border-radius: inherit;
  background-color: #fff;
  width: 100%;
  height: 100%;
}

.magnetic-link .cursor-bg {
  opacity: 0.1;
}

.magnetic-link .cursor-bg {
  opacity: 0.1;
}
script.js
javascript
function initMagneticCursor() {  
  gsap.registerPlugin(Flip);
  
  const cursor = document.querySelector(".cursor");
  if(!cursor) return;
  
  const cursorBg = cursor.querySelector(".cursor-bg");
  
  // First, make the cursor div follow our mouse
  gsap.set(".cursor", {xPercent:-50, yPercent: -50});

  let xTo = gsap.quickTo(".cursor", "x", {duration: 0.6, ease: "power3"});
  let yTo = gsap.quickTo(".cursor", "y", {duration: 0.6, ease: "power3"});

  window.addEventListener("mousemove", e => {
    xTo(e.clientX);
    yTo(e.clientY);
  });
  
  // Then, make the setup for our flipping button
  const hoverTargets = document.querySelectorAll("[data-magnetic-cursor-target]");
  if(!hoverTargets.length) return;
  
  hoverTargets.forEach((target) => {
    const bgHolder = target.querySelector("[data-magnetic-cursor-bg]");
    
    target.addEventListener("mouseenter", () =>{
      // Register the 'state'
      const state = Flip.getState(cursorBg);
      
      // Add bg element into the link
      bgHolder.appendChild(cursorBg); 
      
      // Create the Flip animation
      Flip.from(state,{
        ease:"back.out(1)",
        duration: 0.3
      });
    });

    target.addEventListener("mouseleave", () =>{
      // Register 'state' again, include opacity for subtle ease
      const state = Flip.getState(cursorBg,{
        props:"opacity"
      });
      
      // Add bg element back into the cursor
      cursor.appendChild(cursorBg);
      
      // Flip for a smooth animation
      Flip.from(state,{
        ease:"power4.out",
        duration: 0.5,
      });
    });
  });
}

// Initialize Magnetic Cursor
document.addEventListener('DOMContentLoaded', () => {
  initMagneticCursor();
});

Guide

Implementation

Cursor

Use a fixed positioned element .cursor that follows the pointer via gsap.quickTo, and ensure it contains a single .cursor-bg child that flips between the cursor and targets.

Targets

Use [data-magnetic-cursor-target] to mark any hoverable element that should 'capture' the cursor’s background, enabling the Flip animation from the global cursor into the target.

Background Slot

Place a single child with [data-magnetic-cursor-bg] inside each target; this element acts as the drop-zone where the cursor’s .cursor-bg is appended on hover and Flip-animated into place. This doesn't necessarily have to be a full background, definitely checkout the live preview link to see how you can apply this script.

Flip Behavior

On mouseenter of a target, the script 'captures' state with Flip.getState(cursorBg) and appends the background element into the target’s [data-magnetic-cursor-bg], then Flip animates it by doing all the necessary calculations itself. On mouseleave, it 'captures' the state again, adds .cursor-bg back into .cursor, and flips it out smoothly.

Hover Styling

Style the cursor background while it lives inside a target using a parent selector, so the visual treatment changes only when the Flip has moved it into [data-magnetic-cursor-bg]. Here's an example: