Installation
Install a variant with the shadcn CLI, or copy the files by hand. What lands where, and why the provider is a separate item.
With the shadcn CLI
Register the namespace once in your components.json:
{
"registries": {
"@pointerui": "https://pointerui.dev/r/{name}.json"
}
}Then add any variant:
npx shadcn@latest add @pointerui/dot-ringThat one command writes three things: the variant, the provider it needs, and the engine the
provider needs. GSAP and @gsap/react are installed as dependencies automatically. If you
skip the namespace step, the full URL works on its own:
npx shadcn@latest add https://pointerui.dev/r/dot-ring.jsonWhat lands where
| Item | Files | Location |
|---|---|---|
engine | 8 | lib/pointerui/engine/ |
provider | 6 | components/pointerui/provider/ |
| a variant | 2 | components/pointerui/<slug>/ |
Paths follow the aliases in your own components.json, so a project that puts components
somewhere else gets them there.
Adding a second variant reuses what is already on disk — the engine and provider are separate registry items precisely so that six variants do not mean six copies of the engine.
Wire up the provider
CursorProvider creates the engine and the portal host. It goes once, high in the tree, and
the variant goes inside it:
import { CursorProvider } from "@/components/pointerui/provider";
import { DotRing } from "@/components/pointerui/dot-ring";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<CursorProvider>
<DotRing />
{children}
</CursorProvider>
</body>
</html>
);
}Children render in place — the provider does not portal your app anywhere. Each variant
portals its own layers to document.body, which is what keeps mix-blend-mode working for
blend and keeps every layer clear of your stacking contexts.
To compose variants, mount more than one inside the same provider. They share the engine:
<CursorProvider>
<DotRing />
<TrailDots count={6} />
</CursorProvider>Hiding the native cursor
The provider sets cursor: none on the document — or on the scope element in scoped mode —
while a variant is mounted, and restores it on unmount. You do not need to set it yourself,
and you should not: a stray cursor: none that outlives the component leaves the visitor
with no pointer at all.
Manual install
Copy src/lib/engine/, src/registry/provider/ and the variant folder you want out of the
repo, then fix the two import paths the CLI would have rewritten:
@/lib/engine→ wherever you put the engine../../provider→ wherever you put the provider
Everything else is relative and survives the move.
Vanilla JavaScript
The engine under lib/engine/ is plain TypeScript with GSAP as its only dependency — no
React anywhere in it, enforced by a lint rule. You can drive it directly:
import { createEngine } from "./lib/pointerui/engine";
const engine = createEngine();
const el = document.createElement("div");
document.body.append(el);
const layer = engine.registerLayer(el, { duration: 0.15, ease: "power3" });
const stop = engine.onTick((state) => {
layer.setX(state.x);
layer.setY(state.y);
});Call stop() and layer.destroy() when you are done — the engine removes its gsap.ticker
callback once the last layer unregisters.
A packaged vanilla build is not published yet. The React components are v1; the engine being framework-agnostic from the first commit is what makes that build a packaging job later rather than a rewrite.