Proteus Cursor
Getting started

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>
Getting started

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' });
Core concepts

The two modes

All cursor behaviour builds on one of two base modes, set via the shape option.

shape: 'circle'
Circle

A precise dot with an optional trailing shadow. Supports text, magnetism and blend modes. Hover this card to preview it.

shape: 'fluid'
Fluid

A morphing blob that stretches and squeezes with velocity. Tune it with speed and maxVelocity.

Core concepts

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 propertyTypeDescription
shape_sizestringCSS size, e.g. '60px'.
shape_colorstringCSS color.
hasShadowbooleanShow or hide the shadow.
shadow_sizestringShadow element size.
textstringText inside the cursor.
text_colorstringText color.
text_sizestringCSS font size.
text_weightstringCSS font weight.
blend_modestringPer-state mix-blend-mode.
click_animationstringPer-state click feedback override.
Core concepts

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();
Core concepts

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>
API reference

Constructor options

All options passed to new ProteusCursor(options).

OptionTypeDefaultDescription
shapestring'default'Cursor mode: circle, fluid, or default (native cursor).
shape_sizestring'10px'Width and height of the cursor element.
shape_colorstring'#fff'CSS color of the cursor shape.
hasShadowbooleantrueWhether to show the trailing shadow element (circle mode).
shadow_delaystring'0.3s'CSS transition delay for the shadow lag effect.
shadow_sizestring'40px'Width and height of the shadow element.
shadow_colorstring'#ffffff'Hex color for the shadow glow.
textstring''Text rendered inside the cursor shape.
text_colorstring''Color of the cursor text.
text_weightstring''Font weight of the cursor text.
text_sizestring''Font size of the cursor text.
speednumber0.9Lerp factor for fluid mode movement (0–1). Higher = snappier.
maxVelocitynumber10Maximum velocity cap for fluid deformation.
magneticbooleanfalsePulls the cursor toward the centre of hovered interactive elements (circle mode).
magnetic_strengthnumber0.4Fraction (0–1) of the remaining cursor→centre distance recovered each frame.
magnetic_radiusnumber | nullnullFalloff radius in px from the element centre. null = constant full pull.
magnetic_targetsstring'a, button, …'CSS selector for the elements that attract the cursor. Default includes [data-cursor-magnetic].
magnetic_parallaxbooleanfalseThe hovered element itself leans toward the cursor (max ±10px) and springs back on leave.
magnetic_parallax_strengthnumber0.15Fraction (0–1) of the cursor's offset applied as the element's shift.
blend_modestring'normal'CSS mix-blend-mode on the cursor. Use 'difference' for auto inversion.
click_animationstring'scale'Click feedback: scale, ripple, or none.
click_durationnumber300Duration in ms of the click animation.
respectReducedMotionbooleantrueSkip init when the OS has prefers-reduced-motion: reduce.
reducedMotionFallbackstring'native'Under reduced motion: 'native' (no custom cursor) or 'static' (motionless custom cursor).
API reference

Setters

All setters can be called at runtime. Most accept an optional isPermanent flag — when true, the value persists after state resets.

MethodParametersDescription
setShape(shape)stringSwitch 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, stringResize 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)numberAdjust fluid interpolation speed (0–1).
setMaxVelocity(max)numberCap 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, objectRegister a named state. Chainable.
removeState(name)stringRemove 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.
API reference

Static methods & properties

Class-level utilities. Call without creating an instance.

Method / propertyReturnsDescription
ProteusCursor.isTouchOnly()booleantrue when the primary pointer is coarse (touch device).
ProteusCursor.prefersReducedMotion()booleantrue when the OS has prefers-reduced-motion: reduce.
ProteusCursor.getPreset(name)objectRaw config for a named preset — spread into constructor options.
ProteusCursor.PRESETSobjectAll five preset configs: ghost, neon, minimal, chrome, ink.
Features

Preset system

Five built-in configurations. Apply in one line and override any property. Hover the entries to preview.

Ghost
12px translucent dot, subtle glow.
Neon
Teal dot, wide electric halo.
Minimal
6px dot, no shadow at all.
Chrome
48px circle, blend difference.
Ink
Fluid physics blob, organic feel.
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',
});
Features

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' });
ValueEffect
'normal'Default — no blending.
'difference'Inverts the colors beneath the cursor. Best with a white shape.
'exclusion'Softer inversion, lower contrast.
any CSS valueThe full mix-blend-mode spec is supported.
Features

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
});
Features

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>
Integrations

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.

React
import { useProteusCursor } from 'proteuscursor/react';
import 'proteuscursor/style';

const cursor = useProteusCursor({ shape: 'circle' });
// cursor.current → instance (null until mounted)
Vue 3
import { useProteusCursor } from 'proteuscursor/vue';
import 'proteuscursor/style';

const cursor = useProteusCursor({ shape: 'circle' });
// cursor.value → instance (null until mounted)
Svelte
import { useProteusCursor } from 'proteuscursor/svelte';
import 'proteuscursor/style';

const cursor = useProteusCursor({ shape: 'circle' });
// cursor.current → instance (undefined until mounted)
Accessibility

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();
DevicePrimary pointerProteus active?
Phone / tablet, no mousecoarseNo — skipped.
Laptop with touchscreenfineYes.
iPad / Surface + paired mousefineYes.