documentation
cursorkit
Spring-powered custom cursor effects for React and Next.js. Variant stack, canvas plugins, magnetic effects, idle detection — zero runtime dependencies.
Installation
Install from npm. The only peer dependencies are react and react-dom ≥ 18. No additional runtime libraries required.
npm
yarn
pnpm
Next.js Setup
In the Next.js App Router, place CursorProvider and Cursor inside a Client Component wrapper so they have access to browser APIs. The cleanest pattern is a dedicated Providers component referenced from your root layout.
app/providers.tsx
app/layout.tsx
"use client" in the bundle. Importing them from a Server Component is safe — Next.js treats them as Client Component boundaries automatically. You only need your own "use client" when you use hooks from this package (like useCursorIdle or useMagnetic) in your own components.Quick Start
The minimum setup: CursorProvider wraps your app, Cursor renders the animated cursor element, and CursorTarget adds interactivity to individual elements.
Full example
TypeScript
The package ships its own declaration files. Import types directly from the package root:
Generic type parameters are supported on hooks that return refs:
core
CursorProvider
The context root. Must wrap the entire app (or any subtree that uses cursor effects). Manages the variant stack and distributes cursor state to all descendants. Mouse tracking is handled internally via a singleton — zero 60fps re-renders on consumers.
| prop | type | default | description |
|---|---|---|---|
| config | CursorConfig | built-in | Variant definitions. Each key is a variant name. The default key is required. |
| hideNativeCursor | boolean | true | Injects cursor:none!important on the body while mounted. |
CursorConfig — variant definitions
A CursorConfig object maps variant names to visual and physics properties. The default key is required and acts as the fallback resting state.
Variant stack
CursorProvider maintains a stack of active variants. CursorTarget pushes a variant on mouseenter and pops it on mouseleave, so nested targets compose correctly. You can control the stack imperatively:
Cursor
The visible cursor element. Reads variant state from CursorProvider and animates position via a RAF loop using Velocity Verlet spring integration — never triggers a React re-render during animation. Place it once anywhere inside CursorProvider.
| prop | type | default | description |
|---|---|---|---|
| zIndex | number | 9999 | CSS z-index of the cursor overlay. |
| elongate | boolean | false | Spring-smoothed stretch along the velocity vector while moving. |
CursorTarget
A polymorphic wrapper that pushes a cursor variant on mouseenter and pops it on mouseleave. Also handles focus/blur for keyboard accessibility. All extra props are forwarded to the underlying element unchanged.
| prop | type | default | description |
|---|---|---|---|
| as | ElementType | "div" | HTML tag or component to render as. |
| variant | string | — | Variant name from CursorConfig to push on hover/focus. |
| onEnter | () => void | — | Called when the cursor enters the element. |
| onLeave | () => void | — | Called when the cursor leaves the element. |
useCursor
Returns event handler props to manually wire variant behavior to any element. Useful when you can't use CursorTarget — for example, a component that already manages its own event handlers.
useCursorVariant
Provides imperative control over the variant stack and global variant override. Useful for triggering cursor changes in response to application events (drag start, modal open, etc.).
plugins
Plugins mount a canvas or DOM overlay above the page and respond to mouse and click events. Drop any plugin anywhere inside CursorProvider — they are independent of each other and can be combined freely. All plugins use the shared mouseStore singleton so no extra window listeners are created.
CursorClickEffect
Spawns an animated canvas effect on every mouse click. Also responds to emitCursorEvent("click") for programmatic triggers. The RAF loop is idle between clicks — zero CPU cost when nothing is animating.
| prop | type | default | description |
|---|---|---|---|
| type | string | "ripple" | Effect type — see table below. |
| size | number | 50 | Maximum diameter of the effect in px. |
| duration | number | 600 | Animation duration in milliseconds. |
| color | string | "rgba(255,255,255,0.8)" | Effect colour. Ignored for confetti — use palette instead. |
| strokeWidth | number | 2 | Ring / particle stroke width in px. |
| particleCount | number | 12 | Number of particles or rays emitted per click. |
| palette | string[] | built-in | Array of hex/rgba colours for the confetti type. |
| zIndex | number | 9997 | CSS z-index of the canvas overlay. |
Available effect types
| prop | type | default | description |
|---|---|---|---|
| ripple | — | — | Expanding ring that fades out — the classic click ripple. |
| burst | — | — | Particles radiate outward from the click point. |
| sparkle | — | — | Star-ray lines shoot outward from the click point. |
| shockwave | — | — | Two concentric rings with a stagger — a shockwave feel. |
| confetti | — | — | Coloured rectangles fly outward with gravity. Use the palette prop. |
| implode | — | — | Particles rush inward then explode outward — reversed burst. |
CursorTrail
Renders a trail behind the cursor on a full-screen canvas. Uses a dirty-flag pattern — the RAF loop only runs one frame per mouse movement, then suspends. The canvas retains its last drawn state while idle, so no CPU is wasted.
| prop | type | default | description |
|---|---|---|---|
| type | string | "dots" | Trail style — see table below. |
| color | string | "rgba(255,255,255,0.6)" | Trail colour. |
| size | number | 8 | Dot diameter or stroke width in px. |
| count | number | 12 | Number of positions in the trail (dots / line / glow). |
| fadeTime | number | 1800 | Ink and spark: ms before strokes or particles fade out. |
| spread | number | 60 | Spark: initial particle spread speed in px/s. |
| zIndex | number | 9998 | CSS z-index of the canvas overlay. |
Available trail types
| prop | type | default | description |
|---|---|---|---|
| dots | — | — | Fading circles spaced along the trail. count controls trail length. |
| line | — | — | Tapered Bézier brushstroke that follows the cursor. |
| glow | — | — | Soft radial-gradient halos along the trail path. |
| ink | — | — | A continuous drawn stroke that fades over fadeTime ms. |
| spark | — | — | Each cursor position spawns a drifting particle with gravity. |
CursorSpotlight
A full-screen DOM overlay that follows the cursor with a radial-gradient light effect. Useful for dark hero sections. Set a bright color to illuminate content around the cursor, or a dark color with low opacity for a vignette that clears on hover.
| prop | type | default | description |
|---|---|---|---|
| color | string | "rgba(255,255,255,0.06)" | Spotlight fill colour. Use rgba for transparency. |
| size | number | 400 | Diameter of the lit circle in px. |
| blend | string | "normal" | CSS mix-blend-mode applied to the overlay. |
| zIndex | number | 0 | CSS z-index. Use 0 to stay behind content. |
CursorReveal
A wrapper component that hides children behind a dark overlay and cuts a circular hole following the cursor — revealing the content beneath. Unlike other plugins, this is not standalone: it requires children and wraps them.
| prop | type | default | description |
|---|---|---|---|
| children | ReactNode | — | Content to wrap. The overlay sits on top of it. |
| size | number | 180 | Diameter of the circular reveal hole in px. |
| overlay | string | "rgba(9,9,11,0.92)" | Colour of the overlay that hides the content. |
| blur | number | 0 | Backdrop blur (px) applied to the hidden area. 0 = off. |
| className | string | — | CSS class forwarded to the container div. |
| style | CSSProperties | — | Inline styles forwarded to the container div. |
CursorReveal uses CSS mask-image. For best results, give the container a defined height and overflow: hidden so the overlay fills it completely.CursorDraw
Freehand drawing canvas. Hold the mouse button down to draw strokes. Strokes persist until unmount, or fade over fadeTime ms when set. The RAF loop only runs while drawing or while strokes are actively fading.
| prop | type | default | description |
|---|---|---|---|
| color | string | "rgba(255,255,255,0.85)" | Ink colour. |
| width | number | 2 | Stroke width in px. |
| fadeTime | number | 0 | ms before a completed stroke fades. 0 = permanent. |
| enabled | boolean | true | When false, drawing is paused but existing strokes are preserved. |
| onStroke | function | — | Called with the array of {x, y} points after each stroke completes. |
| zIndex | number | 9996 | CSS z-index of the canvas overlay. |
CursorLens
Wraps children and shows a circular magnifying-glass lens that follows the cursor over the wrapped area. The lens renders a true zoomed copy of the children via DOM duplication and CSS scale — no canvas, no screenshots, no blur artifacts.
| prop | type | default | description |
|---|---|---|---|
| children | ReactNode | — | Content to magnify. Renders twice: normally + scaled inside the lens. |
| size | number | 140 | Lens circle diameter in px. |
| scale | number | 2.2 | Zoom factor. 2 = 2× magnification. |
| border | string | "1.5px solid rgba(255,255,255,0.25)" | CSS border of the lens frame. |
| zIndex | number | 9995 | CSS z-index of the lens overlay. |
hooks
useMagnetic
Attaches a magnetic pull to an element: when the cursor comes within distancepx of the element's center it shifts toward the cursor. Uses mouseStore directly — zero React re-renders in the mouse-tracking path.
| prop | type | default | description |
|---|---|---|---|
| strength | number | 0.35 | Pull intensity: 0 = no pull, 1 = element snaps to cursor center. |
| distance | number | 80 | Activation radius in px from the element's center. |
Returns a RefObject to attach to the target element. The hook writes transform and transition directly on el.style — no wrapper div or extra markup is needed.
useCursorIdle
Automatically switches to a cursor variant after the cursor has been still for timeout ms. Reverts when movement resumes. This hook does not return anything — it manages the variant stack internally.
| prop | type | default | description |
|---|---|---|---|
| timeout | number | 2000 | ms of no cursor movement before idle triggers. |
| variant | string | "spot" | Variant name pushed onto the stack when idle. |
| onIdle | () => void | — | Callback fired when idle state begins. |
| onWake | () => void | — | Callback fired when cursor movement resumes. |
useCursorZone
Returns a ref that, when attached to any element, pushes a variant onto the stack when that element scrolls into view (via IntersectionObserver) and pops it when it scrolls out. Zero configuration beyond the variant name.
| prop | type | default | description |
|---|---|---|---|
| variant | string | — | Variant to push when the element enters the viewport. |
| threshold | number | 0.5 | Fraction of the element that must be visible to trigger (0–1). |
useVelocityCursor
Tracks cursor speed and direction with an exponential moving average (α = 0.12). The RAF loop suspends after ~3 s of near-zero velocity and wakes automatically on the next mouse move.
| prop | type | default | description |
|---|---|---|---|
| idleThreshold | number | 50 | Speed (px/s) below which isMoving becomes false. |
useMousePosition
Returns a live { x, y } mouse position, RAF-throttled to update once per animation frame. Uses the shared mouseStore singleton — no extra mousemove listener is added to the window.
useMousePosition() re-renders ~60fps. For animation loops or canvas drawing where you need position without React re-renders, read mouseStore.x / mouseStore.y directly in a useEffect or RAF callback instead.utilities
themes
Ready-made CursorConfig presets. Pass directly to CursorProvider. Each theme defines variants for default, link, button, text, view, drag, close, spot, invert, and square.
Themes are plain objects — spread and override individual variants to customise:
mouseStore
A module-level singleton that holds the last known cursor position and notifies subscribers via a single shared mousemove listener. All plugins and hooks use it internally. Exported for advanced use cases — custom canvas animations, non-React effects, etc.
| prop | type | default | description |
|---|---|---|---|
| x | number | 0 | Last known clientX. Returns 0 before any mouse move. |
| y | number | 0 | Last known clientY. Returns 0 before any mouse move. |
| subscribe | (fn) => () => void | — | Subscribe to position updates. Returns an unsubscribe function. |
emitCursorEvent
Programmatically triggers a cursor click effect at any screen position without requiring actual user input. Any mounted CursorClickEffect reacts to it automatically via a cursor:click CustomEvent on window.
reference
TypeScript types
All types are exported from the package root and available in dist/ declarations:
CursorVariant fields
| prop | type | default | description |
|---|---|---|---|
| width | number | 24 | Width in px. |
| height | number | 24 | Height in px. |
| scale | number | 1 | CSS scale applied on top of width/height. |
| shape | string | "circle" | "circle" | "square" | "custom" |
| borderRadius | number | — | Explicit border-radius in px (overrides shape). |
| color | string | "rgba(255,255,255,0.8)" | Fill colour. |
| opacity | number | 1 | CSS opacity (0–1). |
| borderColor | string | "transparent" | Border colour. |
| borderWidth | number | 0 | Border width in px. |
| borderStyle | string | "solid" | CSS border-style. |
| mixBlendMode | string | — | CSS mix-blend-mode — e.g. "difference" for invert effect. |
| label | string | ReactNode | — | Text or element rendered centred inside the cursor. |
| fontSize | string | number | "12px" | Font size for the label. |
| fontWeight | string | number | "bold" | Font weight for the label. |
| labelColor | string | "#000" | Label text colour. |
| customElement | ReactNode | — | Replaces the entire cursor element when shape is "custom". |
| innerElements | array | — | Array of { element, transition } for layered elements with independent springs. |
| transition | object | — | Spring: { stiffness, damping, mass, delay }. |
SSR & React Server Components
The package is fully SSR-safe. All browser API usage (window, document, requestAnimationFrame) is guarded by typeof window !== "undefined" checks or deferred to useEffect. The mouseStore singleton lazy-initializes its listener on first subscribe, not at module load.
Both ESM and CJS outputs include "use client" at the top of the bundle. Importing from a Next.js Server Component is safe — Next.js automatically creates the correct client boundary at the import site.
CursorProvider inside a "use client" wrapper component (like the Providers pattern in the Next.js Setup section) rather than directly in a Server Component, so the React context is available to all children.Performance
The package is designed to produce zero React re-renders during cursor animation:
- Single mousemove listener — all consumers share one
mouseStoresubscription. No matter how many plugins are mounted, only one handler fires on each mouse event. - No position in context —
CursorProvidercontext updates only when the active variant or config changes, not on every mouse pixel. This stops 60fps re-renders from cascading to allCursorTargetconsumers. - Direct DOM mutations in RAF —
Cursor,useMagnetic,CursorSpotlight, andCursorLensupdateel.styleinside RAF callbacks, completely bypassing React's reconciler. - Idle RAF suspension — canvas plugins stop their animation loops when there is nothing to render (
CursorClickEffectbetween clicks,CursorTrailwhile the cursor is still,CursorDrawbetween drawing sessions) and restart only on user interaction. - Tree-shaking safe —
"sideEffects": falseinpackage.json. Import only what you use; bundlers eliminate the rest.