Reference

API

Core Class

The Supermouse class is the runtime entry point. It owns the animation loop, input layer, plugin pipeline, and shared MouseState.

Signature
class Supermouse {
  static readonly version: string;

  readonly version: string;
  state: MouseState;
  options: SupermouseOptions;
  get container(): HTMLDivElement;
  get isEnabled(): boolean;

  constructor(options?: SupermouseOptions);

  use(plugin: SupermousePlugin): this;
  getPlugin(name: string): SupermousePlugin | undefined;
  enablePlugin(name: string): void;
  disablePlugin(name: string): void;
  togglePlugin(name: string): void;

  start(): void;
  enable(): void;
  disable(): void;
  destroy(): void;
  step(time: number): void;

  setNativeCursor(type: 'show' | 'hide' | 'auto'): void;
  registerHoverTarget(selector: string): void;
}

Constructor

constructor(options?)

→ Supermouse

new Supermouse(options?: SupermouseOptions)

Creates the runtime, applies options, and prepares the plugin pipeline. If autoStart is left enabled, the animation loop begins immediately during construction. If you set autoStart: false, call start() later to begin the frame loop explicitly.

import { Supermouse } from '@supermousejs/core';
import { Dot } from '@supermousejs/dot';
import { Ring } from '@supermousejs/ring';

const app = new Supermouse({
  smoothness: 0.15,
  hideCursor: true,
  rules: {
    'a, button': { pointer: true },
    '[data-supermouse-magnetic]': { magnetic: true }
  },
  plugins: [Dot({ size: 8 }), Ring({ size: 32 })]
});

// Equivalent imperative registration
// const app = new Supermouse({ smoothness: 0.15 });
// app.use(Dot({ size: 8 })).use(Ring({ size: 32 }));

SupermouseOptions

Passed to the constructor. Each option is documented below.

smoothness

number default 0.15

Physics damping factor between 0 and 1. Lower values feel floatier; higher values snap faster to the target position.

const app = new Supermouse({ smoothness: 0.05 }); // very floaty
const snappy = new Supermouse({ smoothness: 0.35 }); // tight follow

hideCursor

boolean default true

Injects a scoped, high-specificity stylesheet that suppresses the native cursor on the body and registered interactive targets.

const app = new Supermouse({ hideCursor: true });

// Disable if you need the native cursor visible globally
const native = new Supermouse({ hideCursor: false });

enableTouch

boolean default false

When set to true, touch events (e.g., on tablets) are processed instead of being ignored. This enables custom cursor effects on touch devices. Marked as experimental as performance and UX may vary across devices. Setting enableTouch: true without autoDisableOnMobile: false may still result in no touch handling if the device lacks a fine pointer.

// Only enable if you explicitly need touch support
const app = new Supermouse({ enableTouch: true });

ignoreOnNative

'auto' | 'tag' | 'css' | null default 'auto'

Controls when the native cursor is restored over elements that expect it (inputs, text areas, etc.). 'tag' is fastest; 'css' inspects computed cursor styles.

const app = new Supermouse({
  hideCursor: true,
  ignoreOnNative: 'tag' // restore native cursor on inputs & textareas
});

autoDisableOnMobile

boolean default true

Disables the entire system when (pointer: coarse) is detected — phones, tablets, and touch-first devices.

const app = new Supermouse({ autoDisableOnMobile: true });

rules

Record<string, object> default {}

Maps CSS selectors to interaction state objects. The input layer scrapes matched elements and exposes metadata on state.interaction for plugins to consume.

const app = new Supermouse({
  rules: {
    'a, button, [role="button"]': { pointer: true },
    '.card': { magnetic: { strength: 0.4 } },
    'input, textarea': { text: true }
  }
});

data-[prefix]-*

HTML attributes

Per-element overrides that take precedence over rules. Useful for one-off interactions without polluting global configuration. The prefix is configured via dataPrefix (default: 'supermouse').

<!-- Magnetic pull on this card only -->
<div data-supermouse-magnetic data-supermouse-strength="0.6">Hover me</div>

<!-- Force pointer/text affordance -->
<button class="btn-danger" data-supermouse-color="orange">Hover</button>
<textarea data-supermouse-text></textarea>

dataPrefix

string default 'supermouse'

The prefix used for data attributes to store hover metadata and system rules like ignoring cursor injection (data-[prefix]-ignore). Allows multiple instances to coexist without attribute conflicts.

const app = new Supermouse({ dataPrefix: 'my-cursor' });

// HTML becomes:
// <div data-my-cursor-magnetic></div>

container

HTMLElement default document.body

Root element where cursor layers are mounted. Scope the cursor to modals, canvases, or embedded previews.

const wrapper = document.querySelector('#canvas-stage') as HTMLElement;

const app = new Supermouse({
  container: wrapper,
  hideCursor: true
});

plugins

SupermousePlugin[] default []

A list of plugins to load and initialize automatically when the runtime starts.

const app = new Supermouse({
  plugins: [Dot(), Ring()]
});

hoverSelectors

string[] default ['a', 'button', 'input', 'textarea', '[data-hover]', '[data-cursor]']

List of selectors that trigger the custom hover visual and register attributes on state.isHover. When omitted, the runtime uses the built-in defaults shown above. Register additional selectors at runtime with registerHoverTarget().

const app = new Supermouse({
  hoverSelectors: ['.custom-link', '[data-hoverable]']
});

hideOnLeave

boolean default true

When enabled, the runtime clears the cursor back to an off-screen position as soon as the pointer exits the browser viewport. This keeps the stage hidden and prevents stale hover state from lingering after window leave events.

const app = new Supermouse({ hideOnLeave: false }); // keep the last pointer position visible

autoStart

boolean default true

Automatically starts the requestAnimationFrame frame loop. If set to false, you must explicitly invoke app.enable() to start.

const app = new Supermouse({ autoStart: false });
// Load assets/do setup...
app.enable();

resolveInteraction

(el: HTMLElement) => InteractionState

Custom handler to compute interaction states from a hovered element. Bypasses the default data-attribute scraper entirely.

const app = new Supermouse({
  resolveInteraction: (el) => {
    return {
      magnetic: el.classList.contains('magnetic-btn'),
      color: el.getAttribute('data-btn-color')
    };
  }
});

MouseStateapp.state

Mutable state object updated every frame. Logic plugins write to target; visual plugins read smooth for rendering.

pointer

{ x: number, y: number }

Raw coordinates from the latest pointer event before physics smoothing is applied.

const { x, y } = app.state.pointer;

smooth

{ x: number, y: number }

Interpolated coordinates used for rendering. Read this in visual plugins when positioning DOM elements.

const { x, y } = app.state.smooth;
dom.setTransform(el, x, y);

target

{ x: number, y: number }

Goal position for the cursor. Logic plugins (e.g. Magnetic) may mutate this before physics runs.

// Magnetic plugin writes here during update()
app.state.target.x = snappedX;
app.state.target.y = snappedY;

velocity

{ x: number, y: number }

Current movement vector derived from smooth position changes. Useful for squash-and-stretch effects.

import { effects } from '@supermousejs/utils';

const { x: vx, y: vy } = app.state.velocity;
const distortion = effects.getVelocityDistortion(vx, vy);

angle

number

Movement angle in degrees, derived from velocity. Useful for orienting rings, arrows, or rotation-driven cursor effects.

const rotation = app.state.angle;

isNative

boolean

Set to true when the current hover target is being handled natively by the browser, such as form controls or elements with incompatible cursor styles.

if (app.state.isNative) {
  // restore the browser cursor UI for this interaction
}

Indicates whether the runtime has received a valid pointer position at least once. This is the guard that determines whether the stage should become visible.

if (!app.state.hasReceivedInput) {
  // wait for the first pointer move before showing the layer
}

interaction

Record<string, any>

A reactive dictionary containing metadata scraped from the currently hovered element using rules or data attributes.

To prevent "Layout Thrashing" (violating the DOM Firewall), plugins must NEVER query the DOM directly during the high-frequency update loop. Instead, the input layer scrapes this data once on pointer hover and populates state.interaction for plugins to read safely at 60-240fps.

See the Plugin Authoring Guide for a deep-dive on how to use `state.interaction` effectively.

// 1. Define interactive rules at initialization:
const app = new Supermouse({
  rules: {
    '.btn-magnetic': { magnetic: { strength: 0.5 } }
  }
});

// 2. Or define them in HTML directly:
// <button data-supermouse-magnetic data-supermouse-strength="0.8">Hover</button>

// 3. Inside a plugin's update() hook, read from state.interaction:
update(app) {
  const { magnetic, strength } = app.state.interaction;
  if (magnetic) {
    const pull = strength ?? 0.5;
    // Apply pull offset to app.state.target...
  }
}

hoverTarget

HTMLElement | null

The DOM node currently driving the hover interaction, if any.

const el = app.state.hoverTarget;
if (el?.matches('.tooltip-trigger')) {
  // show tooltip plugin
}

isDown

boolean

True while the primary pointer button is pressed.

if (app.state.isDown) {
  scale = 0.9;
}

isHover

boolean

True when the pointer is over a registered selector from rules or registerHoverTarget().

const hovering = app.state.isHover;

forcedCursor

'auto' | 'none' | null

Internal override for native cursor visibility. Usually managed through setNativeCursor().

app.setNativeCursor('show'); // forcedCursor becomes 'auto'
app.setNativeCursor('hide'); // forcedCursor becomes 'none'

shape

ShapeState | null

Defines a specific geometric shape the cursor should morph or conform to (e.g. snapping/sticking to a button container). Storing geometry on the state allows logic plugins (e.g. Stick) to communicate shapes to visual plugins (e.g. Ring) without coupling them.

// 1. In a logic plugin, write target shape coordinates:
app.state.shape = {
  width: hoveredRect.width,
  height: hoveredRect.height,
  borderRadius: 4
};

// 2. In a visual plugin, read and apply the shape:
const shape = app.state.shape;
if (shape) {
  dom.setStyle(el, 'width', `${shape.width}px`);
  dom.setStyle(el, 'height', `${shape.height}px`);
  dom.setStyle(el, 'borderRadius', `${shape.borderRadius}px`);
}

True if the user's OS has preferred reduced motion enabled. Plugins should inspect this flag and disable elaborate transitions, large movements, or particle trails to adhere to accessibility guidelines.

update(app, el) {
  if (app.state.reducedMotion) {
    // Disable floaty spring dynamics, snap instantly
    dom.setTransform(el, app.state.target.x, app.state.target.y);
    return;
  }
  // Standard floaty update...
}

Methods

Public methods on the Supermouse instance.

use(plugin)

→ this

use(plugin: SupermousePlugin): this

Registers a plugin instance. Chainable — call multiple times to layer effects.

import { Dot } from '@supermousejs/dot';
import { Ring } from '@supermousejs/ring';

app.use(Dot({ size: 8 })).use(Ring({ size: 40 }));

enable()

→ void

enable(): void

Resumes input processing and restores the custom-cursor hide behavior. This does not start the loop by itself if the runtime was created with autoStart: false; use start() for that case.

const app = new Supermouse({ autoStart: false });
app.use(Dot());
app.start(); // manually begin the animation loop
app.enable(); // resume input processing

start()

→ void

start(): void

Starts the internal animation loop manually. Useful for plugins that need to resume the loop if it was suspended, or if `autoStart` is false but you only want to start the loop without attaching new DOM events (unlike `enable()`).

app.start();

disable()

→ void

disable(): void

Pauses input processing, performs a hard reset of clearing `shape` and `interaction`, restores the native cursor behavior, and clears the runtime state back to an off-screen position. Plugins remain registered and can be re-enabled later.

app.disable(); // pause input while keeping configuration intact

destroy()

→ void

destroy(): void

Full teardown — removes listeners, destroys plugins, and cleans injected styles. Required before re-initializing on the same page (e.g. route changes in SPAs).

onUnmounted(() => {
  app.destroy();
});

setNativeCursor(type: 'show' | 'hide' | 'auto'): void

Force native cursor visibility for edge cases like text selection or drag-and-drop affordances.

textarea.addEventListener('focus', () => app.setNativeCursor('show'));
textarea.addEventListener('blur', () => app.setNativeCursor('auto'));

getPlugin(name)

→ SupermousePlugin | undefined

getPlugin(name: string): SupermousePlugin | undefined

Retrieves a registered plugin by its name key.

const dot = app.getPlugin('dot');
dot?.setOption?.('size', 12);

enablePlugin(name: string): void

Re-enables a previously disabled plugin and calls its onEnable hook.

app.enablePlugin('ring');

disablePlugin(name: string): void

Disables a single plugin without removing it from the pipeline.

app.disablePlugin('trail');

togglePlugin(name: string): void

Toggles a plugin between enabled and disabled states.

button.addEventListener('click', () => app.togglePlugin('sparkles'));

registerHoverTarget(selector: string): void

Adds a CSS selector to hover detection at runtime.

app.registerHoverTarget('[data-cursor="card"]');

step(time)

→ void

step(time: number): void

Manual frame tick when you control the loop yourself instead of the internal requestAnimationFrame driver.

function frame(now: number) {
  app.step(now);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

SupermousePlugin

The contract that all plugins must fulfill. The core runtime calls these hooks.

install(app: Supermouse): void

Called once when the plugin is registered. Create and mount DOM elements here.

install(app) {
  const el = dom.createCircle(8, 'white');
  app.container.appendChild(el);
  this.el = el;
}

update(app: Supermouse, dt: number): void

Called every frame. Apply transforms and read state here. dt is the frame delta time in milliseconds, matching the runtime loop’s requestAnimationFrame bookkeeping.

update(app, dt) {
  const { x, y } = app.state.smooth;
  dom.setTransform(this.el, x, y);
}

destroy(app: Supermouse): void

Called when the app is destroyed. Remove DOM nodes and release references. When called, it also restores the container's original cursor style.

destroy() {
  this.el?.remove();
  this.el = null;
}

onEnable(app: Supermouse): void

Called when a disabled plugin is re-enabled. Restore visibility and reset state.

onEnable() {
  dom.setStyle(this.el, 'opacity', '1');
}

onDisable(app: Supermouse): void

Called when a plugin is disabled. Hide elements but keep them in the DOM for fast re-enable.

onDisable() {
  dom.setStyle(this.el, 'opacity', '0');
}

@supermousejs/utils

Math & Physics

math.lerp

number

lerp(start, end, factor): number

Linear interpolation. factor 0 returns start, 1 returns end.

import { math } from '@supermousejs/utils';
const value = math.lerp(0, 100, 0.25); // 25

math.damp

number

damp(a, b, lambda, dt): number

Frame-rate independent damping. Higher lambda converges faster.

let pos = 0;
function update(dt) {
  pos = math.damp(pos, target, 12, dt);
}

lerpAngle(start, end, factor): number

Interpolates angles along the shortest path, handling 360° wrap-around.

rotation = math.lerpAngle(rotation, targetAngle, 0.15);

math.dist

number

dist(x1, y1, x2?, y2?): number

Distance between two points, or vector magnitude when x2/y2 are omitted.

const speed = math.dist(vx, vy);

angle(x, y): number

Angle in degrees from the origin to a point.

const direction = math.angle(vx, vy);

clamp(value, min, max): number

Constrains a value between bounds.

const alpha = math.clamp(raw, 0, 1);

random(min, max): number

Random number between min and max (inclusive).

const jitter = math.random(-4, 4);

DOM Manipulation

dom.createActor

→ HTMLElement

createActor(tag?): HTMLElement

Creates a fixed-position element optimized for cursor rendering.

const layer = dom.createActor('div');

dom.createCircle

→ HTMLElement

createCircle(size, color): HTMLElement

Pre-styled circular element — ideal for dots and rings.

const dot = dom.createCircle(8, '#f59e0b');

setTransform(el, x, y, rotation?, scaleX?, scaleY?, skewX?, skewY?): void

Updates transform with automatic center anchoring (translate -50%, -50%).

dom.setTransform(el, x, y, rotation, scaleX, scaleY);

dom.setStyle

→ void

setStyle(el, property, value): void

Writes a style only when the value changes to avoid layout thrashing.

dom.setStyle(el, 'opacity', isVisible ? 1 : 0);

applyStyles(el, styles): void

Bulk-apply multiple styles during initialization.

dom.applyStyles(el, {
  position: 'fixed',
  pointerEvents: 'none',
  zIndex: Layers.CURSOR
});

dom.projectRect

→ DOMRect

projectRect(element, container?): DOMRect

Bounding rect relative to a container — useful for scoped cursors.

const rect = dom.projectRect(target, app.container);

Effects

effects.getVelocityDistortion

→ { rotation, scaleX, scaleY }

getVelocityDistortion(vx, vy, intensity?, maxStretch?)

Squash-and-stretch values derived from velocity — great for motion-reactive cursors.

const { rotation, scaleX, scaleY } = effects.getVelocityDistortion(vx, vy);
dom.setTransform(el, x, y, rotation, scaleX, scaleY);

Constants

Top-most layer for text, tooltips, and critical UI.

el.style.zIndex = Layers.OVERLAY;

Primary cursor layer (dot, pointer).

dot.style.zIndex = Layers.CURSOR;

Secondary followers like rings and brackets.

ring.style.zIndex = Layers.FOLLOWER;

Background effects — trails, particles, sparkles.

trail.style.zIndex = Layers.TRACE;

Fast entrance easing that settles smoothly.

el.style.transition = \`transform 0.4s \${Easings.EASE_OUT_EXPO}\`;

Playful elastic overshoot.

el.style.transition = \`transform 0.6s \${Easings.ELASTIC_OUT}\`;

General-purpose smooth easing.

el.style.transition = \`opacity 0.2s \${Easings.SMOOTH}\`;

Helpers

normalize

→ (state: MouseState) => T

normalize(option, defaultValue): (state) => T

Converts static values, functions, or undefined into a unified getter — removes branching inside update loops.

const getSize = normalize(options.size, 20);
const size = getSize(app.state);

definePlugin

→ SupermousePlugin

definePlugin(config, userOptions): SupermousePlugin

Type-safe helper for authoring plugins with automatic lifecycle wiring.

export const MyRing = (options) =>
  definePlugin({
    name: 'my-ring',
    create: () => dom.createCircle(24, 'white'),
    update: (app, el) => dom.setTransform(el, app.state.smooth.x, app.state.smooth.y)
  }, options);

doctor

→ void

doctor(): void

Debug helper that reports common misconfigurations. Run from the browser console.

import { doctor } from '@supermousejs/utils';
doctor();
supermouse
js
| Copyright © 2024-2026 Stud.io Inc.