Documentation
Proteus Cursor works with any framework or plain HTML. No build tools required for the UMD version.
Installation
npm install proteuscursor
Or, without a bundler, load the UMD build and the minified stylesheet directly:
<link rel="stylesheet" href="dist/proteus-cursor.min.css" /> <script src="dist/proteus-cursor.umd.js"></script>
Quick start
Three lines of code to get your first shapeshifting cursor.
import ProteusCursor from 'proteuscursor';
import 'proteuscursor/style';
const cursor = new ProteusCursor({ shape: 'circle' });
The two modes
All cursor behaviour builds on one of two base modes, set via the shape option.
A precise dot with an optional trailing shadow. Supports text, magnetism and blend modes. Hover this card to preview it.
A morphing blob that stretches and squeezes with velocity. Tune it with speed and maxVelocity.
State machine
Register named cursor states in JavaScript with addState(), then activate them anywhere in HTML with a single data-cursor-state attribute. States revert automatically on mouseleave.
cursor
.addState('cta', {
shape_size: '60px',
shape_color: '#A64B2A',
text: 'Click',
text_size: '13px',
})
.addState('gallery', {
shape_size: '100px',
shape_color: 'rgba(28,25,21,0.1)',
text: 'Zoom',
});
<button data-cursor-state="cta">Buy now</button> <img data-cursor-state="gallery" src="photo.jpg" />
| State property | Type | Description |
|---|---|---|
shape_size | string | CSS size, e.g. '60px'. |
shape_color | string | CSS color. |
hasShadow | boolean | Show or hide the shadow. |
shadow_size | string | Shadow element size. |
text | string | Text inside the cursor. |
text_color | string | Text color. |
text_size | string | CSS font size. |
text_weight | string | CSS font weight. |
blend_mode | string | Per-state mix-blend-mode. |
click_animation | string | Per-state click feedback override. |
Debug overlay
A read-only panel showing the state machine in real time: the active state, the default preset snapshot, current behaviour flags and the last 10 hover/leave transitions. Purely passive — pointer-events: none, re-renders only on transitions.
cursor.enableDebugOverlay(); // top-left
cursor.enableDebugOverlay({ position: 'bottom-right' }); // pick a corner
cursor.disableDebugOverlay();
Data attributes
Per-element overrides without registering a named state — useful for one-off hover effects.
<button data-proteus-shapeSize="80px" data-proteus-shapeColor="#1C1915" data-proteus-text="Hello" data-proteus-textColor="#F3EFE5" data-proteus-textSize="14px" data-proteus-shadowIsEnabled="true" > Hover me </button>
Constructor options
All options passed to new ProteusCursor(options).
| Option | Type | Default | Description |
|---|---|---|---|
shape | string | 'default' | Cursor mode: circle, fluid, or default (native cursor). |
shape_size | string | '10px' | Width and height of the cursor element. |
shape_color | string | '#fff' | CSS color of the cursor shape. |
hasShadow | boolean | true | Whether to show the trailing shadow element (circle mode). |
shadow_delay | string | '0.3s' | CSS transition delay for the shadow lag effect. |
shadow_size | string | '40px' | Width and height of the shadow element. |
shadow_color | string | '#ffffff' | Hex color for the shadow glow. |
text | string | '' | Text rendered inside the cursor shape. |
text_color | string | '' | Color of the cursor text. |
text_weight | string | '' | Font weight of the cursor text. |
text_size | string | '' | Font size of the cursor text. |
speed | number | 0.9 | Lerp factor for fluid mode movement (0–1). Higher = snappier. |
maxVelocity | number | 10 | Maximum velocity cap for fluid deformation. |
magnetic | boolean | false | Pulls the cursor toward the centre of hovered interactive elements (circle mode). |
magnetic_strength | number | 0.4 | Fraction (0–1) of the remaining cursor→centre distance recovered each frame. |
magnetic_radius | number | null | null | Falloff radius in px from the element centre. null = constant full pull. |
magnetic_targets | string | 'a, button, …' | CSS selector for the elements that attract the cursor. Default includes [data-cursor-magnetic]. |
magnetic_parallax | boolean | false | The hovered element itself leans toward the cursor (max ±10px) and springs back on leave. |
magnetic_parallax_strength | number | 0.15 | Fraction (0–1) of the cursor's offset applied as the element's shift. |
blend_mode | string | 'normal' | CSS mix-blend-mode on the cursor. Use 'difference' for auto inversion. |
click_animation | string | 'scale' | Click feedback: scale, ripple, or none. |
click_duration | number | 300 | Duration in ms of the click animation. |
respectReducedMotion | boolean | true | Skip init when the OS has prefers-reduced-motion: reduce. |
reducedMotionFallback | string | 'native' | Under reduced motion: 'native' (no custom cursor) or 'static' (motionless custom cursor). |
Setters
All setters can be called at runtime. Most accept an optional isPermanent flag — when true, the value persists after state resets.
| Method | Parameters | Description |
|---|---|---|
setShape(shape) | string | Switch mode at runtime: 'circle' or 'fluid'. |
setShapeSize(w, h, isPermanent?) | string, string, bool? | Resize the cursor shape. |
setShapeColor(color, isPermanent?) | string, bool? | Change the cursor fill color. |
setShadowEnabled(bool, isPermanent?) | boolean, bool? | Show or hide the shadow element. |
setShadowSize(w, h) | string, string | Resize the shadow blob. |
setShadowColor(hex, alpha?) | string, number? | Set shadow color and optional opacity (0–1). |
setText(text, isPermanent?) | string, bool? | Embed text inside the cursor. |
setTextColor(color, isPermanent?) | string, bool? | Set text color. |
setTextWeight(weight, isPermanent?) | string, bool? | Set font weight of the cursor text. |
setTextSize(size, isPermanent?) | string, bool? | Set font size of the cursor text. |
setSpeed(speed) | number | Adjust fluid interpolation speed (0–1). |
setMaxVelocity(max) | number | Cap maximum fluid deformation. |
setMagnetic(enabled, isPermanent?) | boolean, bool? | Toggle magnetic attraction. Chainable. |
setMagneticStrength(strength, isPermanent?) | number, bool? | Set the pull strength (0–1, clamped). Chainable. |
setMagneticParallax(enabled, isPermanent?) | boolean, bool? | Toggle magnetic parallax. Chainable. |
addState(name, config) | string, object | Register a named state. Chainable. |
removeState(name) | string | Remove a registered state. Chainable. |
setBlendMode(mode, isPermanent?) | string, bool? | Apply CSS mix-blend-mode at runtime. |
loadPreset(name, overrides?) | string, object? | Apply a built-in preset. Chainable. |
enableDebugOverlay(options?) | object? | Show the state-machine debug panel. Chainable. |
disableDebugOverlay() | — | Remove the debug panel. Chainable. |
destroy() | — | Remove cursor elements and all event listeners. |
Static methods & properties
Class-level utilities. Call without creating an instance.
| Method / property | Returns | Description |
|---|---|---|
ProteusCursor.isTouchOnly() | boolean | true when the primary pointer is coarse (touch device). |
ProteusCursor.prefersReducedMotion() | boolean | true when the OS has prefers-reduced-motion: reduce. |
ProteusCursor.getPreset(name) | object | Raw config for a named preset — spread into constructor options. |
ProteusCursor.PRESETS | object | All five preset configs: ghost, neon, minimal, chrome, ink. |
Preset system
Five built-in configurations. Apply in one line and override any property. Hover the entries to preview.
const cursor = new ProteusCursor({ shape: 'circle' });
// apply to a live instance (chainable)
cursor.loadPreset('neon');
cursor.loadPreset('chrome', { shape_size: '64px' }); // with overrides
// use as constructor base
const cursor2 = new ProteusCursor({
...ProteusCursor.getPreset('neon'),
shape_color: '#ff4444',
});
Blend mode
Apply a CSS mix-blend-mode to the cursor shape. 'difference' with a white cursor inverts whatever it crosses — perfect contrast on any background.
new ProteusCursor({
shape: 'circle',
shape_size: '40px',
shape_color: '#ffffff',
hasShadow: false,
blend_mode: 'difference',
});
// runtime or per state
cursor.setBlendMode('exclusion');
cursor.addState('hero', { shape_color: '#ffffff', blend_mode: 'difference' });
| Value | Effect |
|---|---|
'normal' | Default — no blending. |
'difference' | Inverts the colors beneath the cursor. Best with a white shape. |
'exclusion' | Softer inversion, lower contrast. |
| any CSS value | The full mix-blend-mode spec is supported. |
Click animations
Feedback on every mousedown: 'scale' shrinks and springs back, 'ripple' expands a fading circle from the click point, 'none' disables it.
new ProteusCursor({
shape: 'circle',
click_animation: 'ripple', // 'scale' | 'ripple' | 'none'
click_duration: 300, // ms
});
Magnetic behavior
While hovering an interactive element, the cursor is pulled toward its centre and eases back on leave. With magnetic_parallax, the element itself leans a few clamped pixels toward the cursor — a separate opt-in; each works with or without the other.
new ProteusCursor({
shape: 'circle',
magnetic: true,
magnetic_strength: 0.4, // 0–1, snappiness
magnetic_radius: null, // px falloff, null = full pull
magnetic_targets: 'a, button, [data-cursor-magnetic]',
magnetic_parallax: true,
magnetic_parallax_strength: 0.15,
});
// runtime (chainable)
cursor.setMagnetic(true);
cursor.setMagneticStrength(0.6, true);
cursor.setMagneticParallax(true);
Any element can opt in via the data attribute included in the default selector:
<div data-cursor-magnetic>Hover me</div>
Framework adapters
First-class hooks for React, Vue 3 and Svelte via subpath exports. Each handles mount/unmount automatically and is SSR-safe — the constructor runs only on the client.
import { useProteusCursor } from 'proteuscursor/react';
import 'proteuscursor/style';
const cursor = useProteusCursor({ shape: 'circle' });
// cursor.current → instance (null until mounted)
import { useProteusCursor } from 'proteuscursor/vue';
import 'proteuscursor/style';
const cursor = useProteusCursor({ shape: 'circle' });
// cursor.value → instance (null until mounted)
import { useProteusCursor } from 'proteuscursor/svelte';
import 'proteuscursor/style';
const cursor = useProteusCursor({ shape: 'circle' });
// cursor.current → instance (undefined until mounted)
Touch & reduced motion
Inclusive by default. On touch-only devices (detected via pointer: coarse) and under prefers-reduced-motion: reduce, Proteus skips initialization entirely — no DOM elements, no listeners, native cursor preserved. Every API method remains safe to call as a no-op.
// opt out of the reduced-motion check
new ProteusCursor({ shape: 'circle', respectReducedMotion: false });
// or keep a branded, motionless cursor instead of the native one
new ProteusCursor({ shape: 'circle', reducedMotionFallback: 'static' });
cursor.isStaticFallback; // which mode you got
ProteusCursor.isTouchOnly(); // your own branching
ProteusCursor.prefersReducedMotion();
| Device | Primary pointer | Proteus active? |
|---|---|---|
| Phone / tablet, no mouse | coarse | No — skipped. |
| Laptop with touchscreen | fine | Yes. |
| iPad / Surface + paired mouse | fine | Yes. |