PointerUI

Cursors worth following.

A copy-paste library of GSAP-powered mouse followers. Six variants, one engine, 60fps.

BrowseGet started

How it works

  1. 01

    Pick a variant.

    Every demo on this page is the live component, running the same code the CLI will write into your repo.

  2. 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.

  3. 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.ticker callback drives all of them, ref-counted so it is removed when the last layer unregisters — not one requestAnimationFrame loop per component.
  • gsap.quickTo(), never gsap.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 mousemove that 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.

dot-ring/index.tsx — the entire per-frame path
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.

Questions

Is it free?
Yes — MIT, and GSAP itself has been free for commercial use, all plugins included, since April 2025. There is nothing to license and no paid tier.
Do I need React?
For v1, yes. The engine underneath has no React imports at all and is enforced that way by a lint rule, so a vanilla build is a packaging job rather than a rewrite — but it is not published yet.
Does it work with the Next.js App Router?
Yes. Every file in the library starts with 'use client', and this site is a Next 16 App Router app running the components it documents.
Will it slow my site down?
The whole per-frame path is two precompiled setters reading two numbers. There is one listener and one ticker no matter how many variants you mount, and a Playwright test fails the build if a pointer sweep triggers more than a dozen layouts.
How do updates work?
The code is yours the moment it lands. Re-run the install command to pull the latest version of a file, then diff it — nothing updates behind your back.
What about mobile?
Disabled by design. There is no pointer to follow, so the components render nothing; the previews here fall back to a recorded video.