Cursor Image Trail
Visual EffectsA hover region that leaves a trail of images chasing the cursor — images from a pool pop in at pointer positions (throttled by distance) and fade/scale out a moment later, driven by a single rAF loop over a recycled pool of nodes with zero per-frame React state. Under prefers-reduced-motion the trail is disabled entirely and only the children render.
Inspired by Skiper UI — image cursor trail (pattern) — independently built, not affiliated. Attribution policy
Move across me
Installation
npx shadcn@latest add https://parable-three.vercel.app/r/cursor-image-trail.jsonRuns the shadcn CLI — the component source is copied intocomponents/parable/cursor-image-trail.tsx. You own it from there.
Usage
The exact code rendering the preview above.
<CursorImageTrail
className="grid h-80 w-full place-items-center rounded-xl bg-neutral-950"
images={[
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMjQiIGhlaWdodD0iMjU2IiB2aWV3Qm94PSIwIDAgMjI0IDI1NiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjOGI1Y2Y2Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZWM0ODk5Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjIyNCIgaGVpZ2h0PSIyNTYiIHJ4PSIyOCIgZmlsbD0idXJsKCNnKSIvPjwvc3ZnPg==",
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMjQiIGhlaWdodD0iMjU2IiB2aWV3Qm94PSIwIDAgMjI0IDI1NiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZWM0ODk5Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZjVhNjIzIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjIyNCIgaGVpZ2h0PSIyNTYiIHJ4PSIyOCIgZmlsbD0idXJsKCNnKSIvPjwvc3ZnPg==",
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMjQiIGhlaWdodD0iMjU2IiB2aWV3Qm94PSIwIDAgMjI0IDI1NiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMjJkM2VlIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjOGI1Y2Y2Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjIyNCIgaGVpZ2h0PSIyNTYiIHJ4PSIyOCIgZmlsbD0idXJsKCNnKSIvPjwvc3ZnPg==",
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMjQiIGhlaWdodD0iMjU2IiB2aWV3Qm94PSIwIDAgMjI0IDI1NiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZjVhNjIzIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZWM0ODk5Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjIyNCIgaGVpZ2h0PSIyNTYiIHJ4PSIyOCIgZmlsbD0idXJsKCNnKSIvPjwvc3ZnPg==",
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMjQiIGhlaWdodD0iMjU2IiB2aWV3Qm94PSIwIDAgMjI0IDI1NiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjOGI1Y2Y2Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMjJkM2VlIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjIyNCIgaGVpZ2h0PSIyNTYiIHJ4PSIyOCIgZmlsbD0idXJsKCNnKSIvPjwvc3ZnPg==",
]}
spawnDistance={40}
maxVisible={8}
>
<span className="pointer-events-none select-none font-mono text-[11px] uppercase tracking-[0.2em] text-white/45">
Move across me
</span>
</CursorImageTrail>Props
CursorImageTrailProps extends React.HTMLAttributes<HTMLDivElement>
| Prop | Type | Default | Description |
|---|---|---|---|
| images* | string[] | — | Image URLs cycled through as the trail spawns. Required, non-empty. |
| maxVisible | number | 10 | Maximum images alive at once; the oldest gracefully retires past it. |
| spawnDistance | number | 48 | Pointer travel, in px, between spawns. Larger = sparser trail. |