npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@izhann/cursorkit

v1.2.2

Published

Interactive cursor effects for React and Next.js applications — zero runtime dependencies

Readme

@izhann/cursorkit

Spring-powered custom cursor effects for React and Next.js. Zero runtime dependencies.

  • Velocity Verlet spring physics with sub-step integration
  • Variant stack — push/pop variants on hover, reset, or global override
  • 7 canvas plugins (trail, click effects, spotlight, reveal, draw, lens)
  • Magnetic pull, idle detection, scroll-zone switching, velocity tracking
  • 4 built-in themes: minimal, neon, glassmorphism, brutalist
  • Full TypeScript support
  • Works in Next.js App Router (SSR safe — all components are "use client")

Installation

npm install @izhann/cursorkit
# or
yarn add @izhann/cursorkit
# or
pnpm add @izhann/cursorkit

No other dependencies required.


Quick Start

import { CursorProvider, Cursor } from "@izhann/cursorkit"

export default function App() {
  return (
    <CursorProvider>
      <Cursor />
      {/* your app */}
    </CursorProvider>
  )
}

Next.js (App Router)

Put CursorProvider and Cursor in a client component — typically your root layout wrapper:

// components/CursorWrapper.tsx
"use client"
import { CursorProvider, Cursor } from "@izhann/cursorkit"

export function CursorWrapper({ children }: { children: React.ReactNode }) {
  return (
    <CursorProvider>
      <Cursor />
      {children}
    </CursorProvider>
  )
}
// app/layout.tsx  (Server Component — no "use client")
import { CursorWrapper } from "@/components/CursorWrapper"

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <CursorWrapper>{children}</CursorWrapper>
      </body>
    </html>
  )
}

Core API

<CursorProvider>

Provides cursor context to your entire app. Must wrap <Cursor> and any component that uses cursor hooks.

| Prop | Type | Default | Description | |---|---|---|---| | config | CursorConfig | see below | Map of variant names to CursorVariant objects | | hideNativeCursor | boolean | true | Injects cursor: none globally |

const config = {
  default: { width: 20, height: 20, shape: "circle", color: "white" },
  link:    { width: 40, height: 40, color: "transparent", borderColor: "white", borderWidth: 1 },
  button:  { width: 50, height: 50, label: "click", fontSize: "10px", labelColor: "#000" },
}

<CursorProvider config={config} hideNativeCursor>
  <Cursor />
</CursorProvider>

<Cursor>

Renders the animated cursor element. Place once inside CursorProvider.

| Prop | Type | Default | Description | |---|---|---|---| | zIndex | number | 9999 | CSS z-index | | showOnTouch | boolean | false | Render on touch/pointer-coarse devices | | trailLength | number | 10 | History length used for inner-element parallax | | elongate | boolean | false | Squash-and-stretch in direction of travel (circles only) |


<CursorTarget>

Wraps any element. On hover, pushes a variant onto the cursor stack; on leave, pops it.

| Prop | Type | Required | Description | |---|---|---|---| | variant | string | Yes | Variant key defined in your CursorProvider config | | as | ElementType | No | Renders as this element type (default "div") | | onEnter | () => void | No | Called on mouseenter | | onLeave | () => void | No | Called on mouseleave |

import { CursorTarget } from "@izhann/cursorkit"

<CursorTarget as="a" variant="link" href="/about">
  About
</CursorTarget>

<CursorTarget as="button" variant="button" onClick={handleClick}>
  Click me
</CursorTarget>

CursorVariant Type

Every key in your config object is a CursorVariant. All fields are optional.

interface CursorVariant {
  // Size & shape
  width?: number           // px
  height?: number          // px
  scale?: number           // multiplier applied via CSS transform
  shape?: "circle" | "square" | "custom"
  borderRadius?: number    // px — for square shapes

  // Appearance
  color?: string           // background color
  opacity?: number         // 0–1
  borderColor?: string
  borderWidth?: number     // px
  borderStyle?: string
  mixBlendMode?: string    // CSS mix-blend-mode

  // Label
  label?: string | ReactNode
  fontSize?: string | number
  fontWeight?: string | number
  labelColor?: string

  // Custom content
  customElement?: ReactNode  // rendered when shape="custom"
  innerElements?: Array<{
    element: ReactNode
    transition?: { stiffness?: number; damping?: number; mass?: number }
  }>

  // Spring physics
  transition?: {
    stiffness?: number   // default 800  (higher = snappier)
    damping?: number     // default 80   (higher = less bounce)
    mass?: number        // default 1    (higher = more inertia)
  }
}

Example — invert blend mode:

invert: {
  width: 50, height: 50,
  shape: "circle",
  color: "white",
  mixBlendMode: "difference",
}

Themes

Four ready-made configs. Pass directly to CursorProvider:

import { CursorProvider, Cursor, themes } from "@izhann/cursorkit"

<CursorProvider config={themes.neon}>
  <Cursor />
</CursorProvider>

Available themes: "minimal" · "neon" · "glassmorphism" · "brutalist"

Each theme ships with variants: default, link, button, text, view, drag, close, spot, and more.


Plugins

All plugins are canvas- or DOM-based, position: fixed, and render independently of the cursor's spring animation.

<CursorTrail>

Draws a trail behind the cursor.

| Prop | Type | Default | Description | |---|---|---|---| | type | "dots" \| "line" \| "glow" \| "ink" \| "spark" | "dots" | Trail style | | count | number | 12 | Number of trail points (dots/line/glow) | | color | string | "rgba(255,255,255,0.6)" | Trail color | | size | number | 8 | Dot diameter / line width in px | | zIndex | number | 9998 | CSS z-index | | fadeTime | number | 1800 | ms before ink fades (ink/spark only) | | spread | number | 60 | Initial particle velocity for spark type |

<CursorTrail type="ink" color="rgba(99,102,241,0.8)" size={3} />
<CursorTrail type="spark" color="#f472b6" spread={80} />
<CursorTrail type="glow" color="rgba(56,189,248,0.5)" size={12} />

<CursorClickEffect>

Plays a canvas animation on every click.

| Prop | Type | Default | Description | |---|---|---|---| | type | "ripple" \| "burst" \| "sparkle" \| "shockwave" \| "confetti" \| "implode" | "ripple" | Effect style | | size | number | 50 | Maximum diameter in px | | duration | number | 600 | Animation duration in ms | | color | string | "rgba(255,255,255,0.8)" | Effect color | | strokeWidth | number | 2 | Line/particle width in px | | particleCount | number | 12 | Number of particles or rays | | palette | string[] | built-in | Color palette for confetti type | | zIndex | number | 9997 | CSS z-index |

<CursorClickEffect type="confetti" size={80} duration={900} />
<CursorClickEffect type="shockwave" color="rgba(99,102,241,0.9)" />

Also triggered programmatically via emitCursorEvent("click", { x, y }).


<CursorSpotlight>

A radial gradient spotlight that follows the cursor — useful for dark UIs.

| Prop | Type | Default | Description | |---|---|---|---| | color | string | "rgba(255,255,255,0.06)" | Spotlight fill color | | size | number | 400 | Diameter in px | | zIndex | number | 0 | CSS z-index | | blend | string | "normal" | CSS mix-blend-mode |

<CursorSpotlight color="rgba(139,92,246,0.12)" size={500} />

<CursorReveal>

Wraps content with a dark overlay. Moving the cursor cuts a circular hole, revealing what's underneath.

| Prop | Type | Default | Description | |---|---|---|---| | children | ReactNode | — | Content to reveal | | size | number | 180 | Diameter of the reveal circle in px | | overlay | string | "rgba(9,9,11,0.92)" | Color of the overlay | | blur | number | 0 | Backdrop blur in px applied to hidden areas | | className | string | — | Applied to the outer container | | style | CSSProperties | — | Applied to the outer container |

<CursorReveal size={220} overlay="rgba(0,0,0,0.88)" blur={4}>
  <img src="/hero.jpg" alt="Hero" />
</CursorReveal>

<CursorDraw>

Freehand drawing on a fixed canvas overlay.

| Prop | Type | Default | Description | |---|---|---|---| | color | string | "rgba(255,255,255,0.85)" | Ink color | | width | number | 2 | Stroke width in px | | eraseWidth | number | 20 | Eraser diameter in px | | fadeTime | number | 0 | ms before strokes fade (0 = permanent) | | enabled | boolean | true | Pause drawing without unmounting | | zIndex | number | 9996 | CSS z-index | | onStroke | (points: {x,y}[]) => void | — | Called after each draw stroke |

Controls:

  • Left-click drag — draw
  • Right-click drag — erase (eraser circle indicator shown while erasing)
  • Double-click — clear all strokes

Strokes are stored in page coordinates and scroll-anchored — drawings stay attached to page content, not the viewport.

<CursorDraw color="#f472b6" width={3} eraseWidth={30} />
<CursorDraw color="white" width={1.5} fadeTime={3000} />

<CursorLens>

Wraps content and shows a circular magnifying-glass lens that follows the cursor.

| Prop | Type | Default | Description | |---|---|---|---| | children | ReactNode | — | Content to magnify | | size | number | 140 | Lens diameter in px | | scale | number | 2.2 | Zoom factor | | border | string | "1.5px solid rgba(255,255,255,0.25)" | Lens frame border | | zIndex | number | 9995 | CSS z-index |

<CursorLens scale={2.5}>
  <img src="/map.png" alt="Map" style={{ width: "100%" }} />
</CursorLens>

useMagnetic()

Attaches a magnetic pull to any element — when the cursor comes within range, the element shifts toward it.

| Option | Type | Default | Description | |---|---|---|---| | strength | number | 0.35 | Pull strength (0 = none, 1 = full snap) | | distance | number | 80 | Trigger radius in px |

import { useMagnetic } from "@izhann/cursorkit"

function MagneticButton() {
  const ref = useMagnetic<HTMLButtonElement>({ strength: 0.4, distance: 100 })
  return <button ref={ref}>Hover me</button>
}

Hooks

useCursorVariant()

Programmatically push/pop/reset the cursor variant from inside any component.

import { useCursorVariant } from "@izhann/cursorkit"

function MyComponent() {
  const { setCursorVariant, resetCursorToDefault, setGlobalCursorVariant } = useCursorVariant()

  return (
    <div
      onMouseEnter={() => setCursorVariant("link", "push")}
      onMouseLeave={() => setCursorVariant("", "pop")}
    >
      hover me
    </div>
  )
}

Methods:

| Method | Description | |---|---| | setCursorVariant(variant, mode?) | mode: "push" (default), "pop", or "reset" | | resetCursorToDefault() | Clears the stack back to "default" | | setGlobalCursorVariant(variant) | Overrides the entire stack — useful for page-level states | | clearGlobalCursorVariant() | Removes the global override |


useCursorIdle()

Switches to a variant after the cursor stops moving.

| Option | Type | Default | Description | |---|---|---|---| | timeout | number | 2000 | ms of inactivity before idle fires | | variant | string | "spot" | Variant pushed when idle | | onIdle | () => void | — | Called when idle starts | | onWake | () => void | — | Called when movement resumes |

import { useCursorIdle } from "@izhann/cursorkit"

function App() {
  useCursorIdle({ timeout: 3000, variant: "spot", onIdle: () => console.log("idle") })
  return <div>...</div>
}

useCursorZone()

Returns a ref that, when attached to an element, switches the cursor variant based on scroll visibility.

| Option | Type | Default | Description | |---|---|---|---| | variant | string | required | Variant to activate when element is in view | | threshold | number | 0.5 | IntersectionObserver threshold (0–1) |

import { useCursorZone } from "@izhann/cursorkit"

function Section() {
  const ref = useCursorZone<HTMLDivElement>({ variant: "drag", threshold: 0.4 })
  return <div ref={ref}>...</div>
}

useVelocityCursor()

Returns smoothed cursor velocity state, updated via RAF with exponential moving average. Self-suspends when idle.

| Option | Type | Default | Description | |---|---|---|---| | idleThreshold | number | 50 | px/s below which isMoving is false |

Returns VelocityState:

{
  speed: number     // smoothed speed in px/s
  angle: number     // direction in degrees (0=right, 90=down)
  vx: number        // horizontal component in px/s
  vy: number        // vertical component in px/s
  isMoving: boolean // true when speed > idleThreshold
}
import { useVelocityCursor } from "@izhann/cursorkit"

function SpeedDisplay() {
  const { speed, isMoving } = useVelocityCursor({ idleThreshold: 30 })
  return <div>{isMoving ? `${speed} px/s` : "idle"}</div>
}

useMousePosition()

Returns the live cursor position, updated on every mousemove.

import { useMousePosition } from "@izhann/cursorkit"

function Coords() {
  const { x, y } = useMousePosition()
  return <div>{x}, {y}</div>
}

useCursor()

Low-level hook used internally by <CursorTarget>. Returns event handlers to spread onto an element.

import { useCursor } from "@izhann/cursorkit"

function Custom() {
  const handlers = useCursor({
    variant: "link",
    onEnter: () => console.log("in"),
    onLeave: () => console.log("out"),
  })
  return <div {...handlers}>hover</div>
}

Utilities

emitCursorEvent()

Programmatically triggers a CursorClickEffect at any screen position without a real mouse click.

import { emitCursorEvent } from "@izhann/cursorkit"

// Trigger at screen centre
emitCursorEvent("click", { x: window.innerWidth / 2, y: window.innerHeight / 2 })

// Trigger at an element's centre
const rect = buttonRef.current.getBoundingClientRect()
emitCursorEvent("click", { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 })

mouseStore

Singleton that holds the current mouse position and dispatches to all subscribers via a single window.mousemove listener. Used internally by all plugins. Access it directly if you need cursor position outside React without triggering a re-render.

import { mouseStore } from "@izhann/cursorkit"

// Read current position
console.log(mouseStore.x, mouseStore.y)

// Subscribe to movement
const unsub = mouseStore.subscribe((x, y) => {
  myCanvas.style.backgroundPosition = `${x}px ${y}px`
})

// Unsubscribe when done
unsub()

Full Example

"use client"
import {
  CursorProvider, Cursor, CursorTarget,
  CursorTrail, CursorClickEffect, CursorSpotlight,
  useMagnetic, themes,
} from "@izhann/cursorkit"

export default function App() {
  const magnetRef = useMagnetic<HTMLButtonElement>({ strength: 0.4 })

  return (
    <CursorProvider config={themes.minimal}>
      <Cursor elongate />
      <CursorTrail type="dots" color="rgba(255,255,255,0.4)" />
      <CursorClickEffect type="ripple" />
      <CursorSpotlight size={500} color="rgba(255,255,255,0.05)" />

      <main>
        <CursorTarget as="a" variant="link" href="/">Home</CursorTarget>
        <button ref={magnetRef}>Magnetic button</button>
      </main>
    </CursorProvider>
  )
}

License

MIT