webdevpuneet.com

GSAP quickTo Cursor: Magnetic Frame, Labels & Stretch

GSAP quickTo Cursor · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

Requires
<div class="qtc-stage" id="qtcStage">
  <div class="qtc-head">
    <span class="qtc-eyebrow">gsap.quickTo · custom cursor</span>
    <h2 class="qtc-title">Selected work</h2>
  </div>

  <div class="qtc-cards">
    <button type="button" class="qtc-card qtc-c1" data-cursor="view" data-label="View">
      <span class="qtc-num">01</span>
      <span class="qtc-name">Aurora brand system</span>
      <span class="qtc-tag">Identity · 2026</span>
    </button>
    <button type="button" class="qtc-card qtc-c2" data-cursor="play" data-label="▶ Play">
      <span class="qtc-num">02</span>
      <span class="qtc-name">Studio reel</span>
      <span class="qtc-tag">Motion · 1:12</span>
    </button>
    <button type="button" class="qtc-card qtc-c3" data-cursor="drag" data-label="← Drag →">
      <span class="qtc-num">03</span>
      <span class="qtc-name">Field notes</span>
      <span class="qtc-tag">Gallery · 24 shots</span>
    </button>
  </div>

  <button type="button" class="qtc-cta" data-cursor="frame">Start a project <span aria-hidden="true">→</span></button>

  <p class="qtc-hint">Move fast and the ring stretches. Hover a card for a label, hover the button and the ring frames it. Click for a ripple.</p>

  <div class="qtc-dot" id="qtcDot"></div>
  <div class="qtc-ring" id="qtcRing"><span id="qtcLabel"></span></div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Nine quickTo setters
Dot x, y, scaleX, scaleY and ring x, y, rotation, scaleX, scaleY from one set of reusable tweens.
Speed stretch
The ring turns to the travel direction and stretches along it, then relaxes to a circle when you stop.
Four cursor modes
View, Play and Drag lenses plus a frame, chosen with a data-cursor attribute.
Magnetic frame
The ring hugs a hovered button and leans slightly toward the pointer.
Press and ripple
The dot shrinks while the button is down and a click sends out a ripple.
No stray dot on load
Both followers stay invisible until the first mouse move and fade out when the pointer leaves.
Delegated hover handling
One mouseover listener on the stage covers every present and future data-cursor element.
Touch, reduced-motion and no-CDN fallbacks
Native cursor on touch screens, instant follow with reduced motion, and a working page if GSAP is missing.

About this UI Snippet

GSAP quickTo Cursor — One Retargeted Tween per Property, Four Cursor Modes

Screenshot of the GSAP quickTo Cursor snippet rendered live

A custom cursor lives or dies on its follow physics and on what happens under the hood at 120 mousemove events a second. Calling gsap.to() on every move creates a tween object, resolves overwrites and throws the tween away milliseconds later. gsap.quickTo() is the purpose-built answer: build one tween per property up front, then keep feeding it new destinations. This snippet pushes that idea further than a dot and a ring: nine properties run on quickTo, the ring stretches along the direction of travel, and four cursor modes switch with a data attribute.

quickTo: one tween, retargeted forever

gsap.quickTo(dot, 'x', { duration: 0.18, ease: 'power3' }) returns a function. Calling dotX(482) creates nothing: it retargets the existing tween, re-easing from the current position and velocity. Per mousemove the cost is a function call and a number. Here nine setters are created once: dot x, y, scaleX and scaleY, ring x, y, rotation, scaleX and scaleY. quickTo works for any numeric property GSAP can tween, not only position, but it needs the real property name: pass scaleX and scaleY rather than the scale shorthand.

The lag gap is the whole feel

The dot uses 0.18s and the ring 0.55s with the same power3 ease. The gap stretches the pair apart in motion and lets the ring glide back in after every stop. Each retarget keeps its momentum, which is why it feels smoother than a pos += (target - pos) * 0.1 loop running in requestAnimationFrame forever.

Speed stretch along the direction of travel

On each move the script reads movementX and movementY, converts them to an angle and sets the ring's rotation. Because GSAP applies rotation before scale, scaleX then stretches the ring along the way you are moving, and scaleY squashes it a little. The angle is unwrapped to the shortest turn, so crossing 180° never spins the ring a full circle. A 90ms timer relaxes it back to a circle when you stop.

Four cursor modes from a data attribute

Add data-cursor="view | play | drag | frame" to any element and one delegated mouseover handler sets a class on the ring: a frosted View lens, a pink Play lens, a pill with "← Drag →", or a frame. The label text comes from data-label, and the dot shrinks away inside a lens so the text stays readable. Discrete state (the lens) is CSS; continuous motion is quickTo, and the two never fight because they animate different properties.

Magnetic frame on buttons

In frame mode the ring takes the hovered element's size from two CSS variables and stops following the pointer. Its quickTo targets become the element's centre plus 18% of the pointer's offset, so the frame hugs the button and leans toward the pointer. It is the same pair of setters as before with different numbers, which is the point of quickTo.

Press, ripple and leaving the window

Holding the mouse button shrinks the dot through its own scale setter, and a click adds a ripple that GSAP animates and then removes from the DOM. When the pointer leaves the window both followers fade out, and they stay invisible until the first move, so nothing sits in the top-left corner on load.

Touch, reduced motion and no CDN

The cursor is a hover construct. The script only runs for (hover: hover) and (pointer: fine), and only then adds the class that hides the native cursor, so phones keep theirs. With prefers-reduced-motion every duration drops to almost nothing (quickTo ignores a duration of exactly 0) and the stretch and ripple are skipped. If GSAP fails to load the page simply keeps the normal cursor.

Related: the vanilla-lerp custom cursor, attraction physics in magnetic button, pointer-reactive cards in 3d card tilt, and image-trailing in hover image trail.

Step by step

How to Use

  1. 1
    Add the GSAP CDNCore gsap only: quickTo is built in, no plugin needed.
  2. 2
    Add a dot, a ring and a stageTwo position: fixed, pointer-events: none elements. The script centres them with xPercent and yPercent.
  3. 3
    Create the setters onceOne gsap.quickTo per property: x, y, scaleX and scaleY on the dot, x, y, rotation, scaleX and scaleY on the ring.
  4. 4
    Feed them on mousemoveCall the setters with clientX / clientY. Do not call gsap.to inside the handler.
  5. 5
    Tune the two durationsThe 0.18s dot and 0.55s ring gap defines the personality; widen it for a floatier feel.
  6. 6
    Mark elements with data-cursorUse view, play, drag or frame, plus data-label for the text. One delegated mouseover handler does the rest.
  7. 7
    Keep touch and reduced motion in mindThe cursor only starts for hover-capable pointers, and prefers-reduced-motion switches the trail off.

Real-world uses

Common Use Cases

Portfolio and agency sites
Show a View lens over project cards and a Play lens over reels, with a dot that follows instantly and a ring that trails for a premium feel.
Magnetic call-to-action buttons
Frame the main button with the ring and let it lean toward the pointer, alongside a magnetic button that pulls itself toward the cursor.
Galleries and sliders
Switch to a Drag pill over a carousel or image row, or add a trailing preview with the hover image trail.
Interactive product tours
Label hotspots with short words such as Zoom or Open, and combine the pointer position with a 3D card tilt on the cards underneath.
Learning gsap.quickTo
See one API drive position, rotation, scale and stretch, and compare it with the simpler custom cursor built on a lerp loop.

Got questions?

Frequently Asked Questions

Mousemove can fire 120 times a second, and a new gsap.to each time creates a tween, resolves overwrites and discards it. quickTo builds one tween per property once and returns a function that only retargets it, so each event costs a function call and a number.

The script converts movementX and movementY to an angle and sets the ring's rotation, then raises scaleX. GSAP applies rotation before scale, so the stretch runs along the travel direction. A 90ms timer sets the scale back to 1 when the pointer stops.

Angles jump from 180° to -180° when you cross the left direction, and tweening that jump would spin the ring a full turn. Adding the shortest signed difference to a running value keeps the rotation continuous, so it always takes the shortest way round.

Add data-cursor="name" and data-label="Text" to an element, then style .qtc-ring.is-name in CSS for size, colour and shape. The delegated mouseover handler applies the class, so no new JavaScript is needed unless the mode changes where the ring points.

On touch the script never starts, so the page keeps its normal cursor. With prefers-reduced-motion every quickTo duration drops to 0.001s, which makes the followers snap to the pointer, and the stretch and ripple are skipped. If GSAP fails to load, nothing changes.

Create the quickTo setters and the listeners in a mount effect against refs, and remove the listeners in the cleanup. Never route coordinates through component state, because that would re-render on every mousemove. Mode changes happen rarely, so they can safely use state.