Cursors worth following.
A copy-paste library of GSAP-powered mouse followers. Six variants, one engine, 60fps.
The library — every card is the live component
Dot + ring
Ring lags behind with spring
Magnetic
Morphs onto element bounds
Spotlight
Radial reveal follows the pointer
Blend
Inverts whatever it crosses
Trail dots
Eight layers, staggered lag
Trail canvas
Pooled particles, additive blend
No videos, no GIFs. Each panel mounts the real component and scopes it to that card, which is why the native cursor comes back the moment you leave one.
How it works
- 01
Pick a variant.
Every demo on this page is the live component, running the same code the CLI will write into your repo.
- 02
Run one command.
The shadcn CLI pulls the source in and installs GSAP for you. There is no package to pin and nothing to keep in sync.
- 03
Make it yours.
The code is in your project from day one. Tune the easing, restyle the layers, gut what you do not need.
Why GSAP
Built for an event that never stops.
- One ticker for every layer. A single
gsap.tickercallback drives all of them, ref-counted so it is removed when the last layer unregisters — not onerequestAnimationFrameloop per component. gsap.quickTo(), nevergsap.to(). A tween per pointer event is hundreds of allocations a second. These setters are compiled once and then only write numbers.- Pointer state never enters React. Moving the mouse causes zero re-renders. A
mousemovethat triggers one is the most common way this pattern goes from 60fps to 20.
Verified in CI: a Playwright test sweeps the pointer across the page and fails if the browser performs more than a dozen layouts.
const offTick = engine.onTick((state) => {
if (!state.isVisible) return;
if (!revealed) {
revealed = true;
// Jump to position before fading in, so the reveal is not a flight.
gsap.set([dotEl, ringEl], { x: state.x, y: state.y });
gsap.to([dotEl, ringEl], { opacity: 1, duration: 0.2 });
}
dot.setX(state.x);
dot.setY(state.y);
ring.setX(state.x);
ring.setY(state.y);
});One engine, many cursors
Composed
dot-ring and trail-dots on one engine
Composition, not conflict.
Mount two variants inside the same provider and they cooperate, because neither owns the pointer — the engine does. That is eight animated layers above, driven by one listener, one ticker and one shared state object.
Two independent followers, each with its own loop, would fight over cursor: none and drift out of sync within seconds.
Accessibility & performance
Respects reduced motion
Durations collapse to zero, so the cursor still renders but stops lagging. One prop switches it to rendering nothing at all.
Disabled on touch
Without a fine pointer the engine is inert: no listeners, no ticker, no layers. Phones pay nothing.
Never blocks clicks
Every layer is pointer-events: none and portalled out of your tree, so it cannot swallow a hover or a click.
Never hides focus
Focus rings are untouched and aria-hidden on every layer. The cursor is decoration and is treated as such.