KQ-1407 · PHANTOM/CURSOR
Phantom Cursor
A shadow self that follows the pointer inside its box: a soft form of rising smoke shaped like the pointer's own arrow (or a round wisp), drawn by one WebGL2 shader scissored to its own bounds. It trails on a 2D spring that slides from springs.glide to one softer than springs.drift, smearing along its travel, then stiffens up to five times and hurries in to condense under the cursor once the hand rests, and dissolves after an idle moment. Keyboard focus inside sends it to rest on the focused element's corner.
Waylight · night edition
Somebody is right behind you.
Short stories for the walk home, read aloud after dark.
phantom asleep · move across the card, or tab to it
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/phantom-cursor
Props
- children
- ReactNode
- default —
- The surface the phantom haunts; it stays fully interactive.
- lag
- number
- default 0.6
- How far it lags the hand: 0 keeps close on the glide spring, 1 trails on a spring softer than drift.
- size
- number
- default 28
- The phantom's height, in CSS pixels.
- smoke
- number
- default 0.6
- How much of it is smoke: 0 is a soft silhouette, 1 a loose, drifting wisp.
- shape
- "arrow" | "orb"
- default "arrow"
- A shadow of the pointer's own arrow, or a round wisp.
- stretch
- number
- default 0.6
- How far it smears behind itself along its travel, 0 to 1.
- catchUp
- number
- default 0.6
- How much faster it closes in once the hand rests, 0 (never hurries) to 1 (five times stiffer).
- idle
- number
- default 1400
- How long it lingers once caught up and still, in ms, before it dissolves; 0 never dissolves while the pointer is inside.
- offset
- [number, number]
- default [8, 10]
- Where it rests relative to the pointer, in CSS pixels; a shadow falls below and to the right.
- color
- string
- default "var(--ink)"
- Its colour, any CSS colour, resolved on the element.
- opacity
- number
- default 0.45
- Its peak opacity, 0 to 1.
- followFocus
- boolean
- default true
- Keyboard focus inside sends the phantom to the focused element.
- interactive
- boolean
- default true
- The pointer (mouse and pen) summons it; touch gets the resting state.
- paused
- boolean
- default false
- Holds the smoke's drift; the phantom still follows.
- seed
- number
- default 3
- Seed for the smoke.
- dpr
- number
- default 2
- The highest device pixel ratio the canvas draws at (never above 2).
- className
- string
- default —
- Classes for the box.
- ref
- Ref<PhantomCursorHandle>
- default —
- Exposes appear(x?, y?), which shows the phantom resting at a point in the box (its centre when omitted) until it dissolves; ignored while the visitor's pointer is inside.
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | The surface the phantom haunts; it stays fully interactive. |
| lag | number | 0.6 | How far it lags the hand: 0 keeps close on the glide spring, 1 trails on a spring softer than drift. |
| size | number | 28 | The phantom's height, in CSS pixels. |
| smoke | number | 0.6 | How much of it is smoke: 0 is a soft silhouette, 1 a loose, drifting wisp. |
| shape | "arrow" | "orb" | "arrow" | A shadow of the pointer's own arrow, or a round wisp. |
| stretch | number | 0.6 | How far it smears behind itself along its travel, 0 to 1. |
| catchUp | number | 0.6 | How much faster it closes in once the hand rests, 0 (never hurries) to 1 (five times stiffer). |
| idle | number | 1400 | How long it lingers once caught up and still, in ms, before it dissolves; 0 never dissolves while the pointer is inside. |
| offset | [number, number] | [8, 10] | Where it rests relative to the pointer, in CSS pixels; a shadow falls below and to the right. |
| color | string | "var(--ink)" | Its colour, any CSS colour, resolved on the element. |
| opacity | number | 0.45 | Its peak opacity, 0 to 1. |
| followFocus | boolean | true | Keyboard focus inside sends the phantom to the focused element. |
| interactive | boolean | true | The pointer (mouse and pen) summons it; touch gets the resting state. |
| paused | boolean | false | Holds the smoke's drift; the phantom still follows. |
| seed | number | 3 | Seed for the smoke. |
| dpr | number | 2 | The highest device pixel ratio the canvas draws at (never above 2). |
| className | string | — | Classes for the box. |
| ref | Ref<PhantomCursorHandle> | — | Exposes appear(x?, y?), which shows the phantom resting at a point in the box (its centre when omitted) until it dissolves; ignored while the visitor's pointer is inside. |