cursor-as-tool
v0.1.2
Published
Framework-agnostic dynamic cursor library — cursor transforms contextually based on the hovered element
Maintainers
Readme
cursor-as-tool
Framework-agnostic dynamic cursor library — the cursor transforms contextually based on the element it hovers over.
What it does
cursor-as-tool replaces the browser's native cursor with a custom, context-aware one. The cursor icon changes automatically based on what's under it:
| Element | Cursor shown |
| ------------------------------------------------------ | --------------- |
| <video> (paused) | ▶ Play |
| <video> (playing) | ⏸ Pause |
| input[type=range], .slider, .swiper, .carousel | ⟵ ⟶ Slider |
| External <a href> (different domain) | ↗ External link |
| <button>, internal link, [role=button] | Pointer |
| <input>, <textarea>, [contenteditable] | Text cursor |
| [draggable=true] | Drag |
| [disabled], [aria-disabled=true] | Blocked |
| Everything else | Default |
Custom rules let you override any selector to any cursor type.
Install
npm install cursor-as-toolUsage
Vanilla JS / TypeScript
import { CursorAsTool } from 'cursor-as-tool'
const cursor = new CursorAsTool({
smooth: true, // smooth mouse-following (lerp)
smoothSpeed: 0.15, // 0–1, lower = more lag
size: 32, // cursor size in px
zIndex: 9999,
theme: 'gaming', // 'minimal' | 'gaming'
})
cursor.init()
// Later, clean up:
cursor.destroy()One-liner convenience:
import { initCursor } from 'cursor-as-tool'
const cursor = initCursor({ smooth: true, theme: 'gaming' })
// cursor is already activeReact
import { useCursorAsTool } from 'cursor-as-tool/react'
function App() {
// init on mount, destroy on unmount — automatic
useCursorAsTool({ smooth: true, theme: 'gaming' })
return <main>Your app</main>
}CDN (no bundler)
<script src="https://unpkg.com/cursor-as-tool/dist/cursor-as-tool.umd.global.js"></script>
<script>
// Quick start
CursorAsTool.init()
// With options
const cursor = new CursorAsTool.CursorAsTool({ smooth: true, theme: 'gaming' })
cursor.init()
</script>Themes
- minimal: clean and subtle style
- gaming: neon style with stronger visual effects
import { CursorAsTool } from 'cursor-as-tool'
new CursorAsTool({ theme: 'minimal' }).init()
new CursorAsTool({ theme: 'gaming' }).init()Configuration
All options are optional. The defaults work out of the box.
interface CursorConfig {
/**
* Hide the native browser cursor.
* @default true
*/
hideNativeCursor?: boolean
/**
* Custom cursor size in pixels.
* @default 32
*/
size?: number
/**
* CSS z-index of the cursor element.
* @default 9999
*/
zIndex?: number
/**
* Enable smooth (lerp) cursor movement.
* @default false
*/
smooth?: boolean
/**
* Smoothing speed: 0 = maximum lag, 1 = no smoothing.
* Only applies when smooth is true.
* @default 0.15
*/
smoothSpeed?: number
/**
* Cursor icon theme.
* @default 'minimal'
*/
theme?: 'minimal' | 'gaming'
/**
* Custom cursor rules — evaluated before automatic detection.
* First matching rule wins.
* @default []
*/
cursorRules?: CursorRule[]
/**
* Scope the cursor to a specific container element.
* @default document.body
*/
container?: HTMLElement
}Custom cursor rules
Override automatic detection for any CSS selector:
const cursor = new CursorAsTool({
cursorRules: [
{ selector: '.gallery img', cursorType: 'zoom-in' },
{ selector: '.delete-btn', cursorType: 'disabled' },
{ selector: '[data-loading]', cursorType: 'loading' },
],
})Inline data-cursor attribute
You can also set the cursor type inline using a data-cursor attribute — no JavaScript needed:
<div class="gallery-item" data-cursor="zoom-in">
<img src="photo.jpg" />
</div>Available cursor types
| Value | Description |
| --------------- | ------------------------------------- |
| default | Plain arrow |
| pointer | Clickable hint |
| text | I-beam for text fields |
| play | Triangle (▶) for paused video |
| pause | Pause bars (⏸) for playing video |
| slider | Horizontal arrows for sliders |
| external-link | Arrow-and-box for external links |
| drag | Four-way arrow for draggable elements |
| grabbing | Closed hand while dragging |
| zoom-in | Magnifying glass with + |
| zoom-out | Magnifying glass with − |
| disabled | Circle with slash |
| loading | Spinner ring |
API
CursorAsTool
class CursorAsTool {
constructor(config?: CursorConfig)
/** Create DOM element and start listening to events. */
init(): void
/** Remove DOM element and clean up all event listeners. */
destroy(): void
/** Returns the cursor type currently on screen. */
getCurrentCursorType(): CursorType
}initCursor(config?) — convenience factory
Equivalent to new CursorAsTool(config).init(). Returns the instance.
useCursorAsTool(config?) — React hook
Calls init() on mount and destroy() on unmount. Returns the stable instance.
Styling
The cursor element has the id cursor-as-tool-cursor and the attribute data-cursor-type set to the current type. Use CSS to style it:
#cursor-as-tool-cursor {
color: white; /* SVG icon color (uses currentColor) */
mix-blend-mode: difference;
}
/* Change color per type */
#cursor-as-tool-cursor[data-cursor-type='play'] {
color: #ff5252;
}
#cursor-as-tool-cursor[data-cursor-type='external-link'] {
color: #69f0ae;
}SSR / Next.js / Astro
The library checks typeof window !== 'undefined' internally. All DOM operations are guarded. Safe to import in SSR environments — just call init() on the client:
// Next.js app router
'use client'
import { useCursorAsTool } from 'cursor-as-tool/react'// Astro component script
import { onMount } from 'solid-js' // or your preferred pattern
// In Astro, use client:load directive on the componentPerformance
- Cursor position update is synchronised with
requestAnimationFrame(smooth mode) or applied directly onmousemove(default). - Element detection is O(depth of DOM tree) per hover — typically < 1ms.
- Zero re-renders, zero dependencies, zero global state.
- Respects
prefers-reduced-motion: setsmooth: falseto honour user preferences.
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches
const cursor = new CursorAsTool({ smooth: !prefersReducedMotion })Mobile / touch devices
The library only activates when a mouse/pointer is present. On pure touch devices the cursor element is hidden automatically via the mouseleave fallback. No action is needed on your part.
Browser support
All modern browsers supporting ES2020+ (Chrome 80+, Firefox 74+, Safari 13.1+, Edge 80+).
