Advanced

Tips & Tricks

Design Philosophy Tip
Supermouse is built on a modular design. To build amazing cursor experiences, stack multiple simple plugins together rather than compiling heavy multi-purpose layers. Keep UI transitions instantaneous and physics smooth to create high-precision, premium interfaces.

Plugin Architecture & Design

Choose the Right Plugin Template

Use definePlugin helper for standard visual widgets (like circles, icons, or rings) that represent single DOM entities. It manages configuration watches, mounts, style synchronization, and cleanups for you.

export const MyPlugin = (options) =>
  definePlugin({
    name: 'my-plugin',
    create: (app) => { /* return DOM element */ },
    update: (app, el) => { /* update element */ }
  }, options);

Use plain objects for raw performance hooks, plugins with multiple DOM elements, custom canvas contexts, dynamic routing bounds, or logic-only scripts.

export const MyPlugin = (): SupermousePlugin => {
  let el = null;

  return {
    name: 'my-plugin',
    install(app) { /* setup */ },
    update(app) { /* frame update */ },
    destroy() { /* cleanup */ }
  };
};

Priority Execution Order

Logic plugins must use negative priority (e.g. -10). These write to state.target (directing where the cursor should go) and must run before physics calculations. Using positive or zero priority for positioning updates causes frame lag ("tearing") as rendering occurs before targets stabilize.

definePlugin({
  name: 'my-logic',
  priority: -10,  // ✅ Logic plugins always negative
  update(app) {
    app.state.target.x += 5;
  }
}, options);

Visual plugins use zero or positive priority. These hooks run after logic and physics steps to read the interpolated state.smooth coordinates and update styling.

definePlugin({
  name: 'my-visual',
  priority: 0,  // ✅ Visual plugins non-negative
  update(app, el) {
    const { x, y } = app.state.smooth;
    dom.setTransform(el, x, y);
  }
}, options);

State Management & Inter-Plugin Communication

Share Geometry via state.shape

Avoid redundant DOM rect measurements. Storing bounds in the shared state.shape container allows logic layers (like Stick) to communicate sizes to rendering layers (like Ring) without directly coupling their classes.

Logic Plugin (Stick):

// Calculates geometry, writes to state.shape
app.state.shape = {
  width: rect.width + padding,
  height: rect.height + padding,
  borderRadius: parseFloat(style.borderRadius)
};

Visual Plugin (Ring):

// Reads geometry, morphs accordingly
if (app.state.shape) {
  el.style.width = `${app.state.shape.width}px`;
  el.style.height = `${app.state.shape.height}px`;
} else {
  el.style.width = '20px';  // fallback circle
}

Use state.interaction instead of DOM Reads

Do not query DOM nodes inside the hot update loop. The input system scrapes attributes from active hovered elements and registers them inside the O(1) state.interaction store, avoiding heavy reflow calculations.

// ❌ WRONG - Causes layout thrashing every frame
update(app) {
  const color = app.state.hoverTarget?.getAttribute('data-color');
}
// ✅ CORRECT - Pre-cached from mouseover
update(app) {
  const color = app.state.interaction.color;
}

Define interactive targets in HTML and read them reactively:

export const MyPlugin = (options) => {
  return definePlugin({
    install(app) {
      app.registerHoverTarget('[data-supermouse-myplugin]');
    },
    update(app) {
      const val = app.state.interaction.myplugin;
      if (val) {
        // Plugin is active for current hover target
      }
    }
  }, options);
};

Performance Optimization

1. Adhere to the DOM Firewall

Calling properties like getBoundingClientRect() inside high-frequency frames triggers layout thrashing. Perform measurements inside event listener updates or cache them on hover target changes.

update(app) {
  const rect = app.state.hoverTarget?.getBoundingClientRect();
  const style = window.getComputedStyle(app.state.hoverTarget!);
}

✅ Correct - Cache calculations on target shift:

let cachedRect = null;

update(app) {
  if (app.state.hoverTarget && app.state.hoverTarget !== lastTarget) {
    lastTarget = app.state.hoverTarget;
    cachedRect = dom.projectRect(app.state.hoverTarget, app.container);
  }
  if (cachedRect) {
    // Use cached values
  }
}

2. Frame-Rate Independent Damping

Ensure velocities and spring timings account for high-refresh screens (e.g. 144Hz). Always use the frame delta time (dt) argument or the math.damp helper.

current += (target - current) * 0.1;

✅ Correct - Use the damping utility:

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

const damping = 10;
current = math.damp(current, target, damping, deltaTime);

3. Allocation Discipline

Creating objects (`{ x, y }`) inside the render loop triggers garbage collection pauses. Reuse persistent local objects or compute indices inline.

update(app) {
  const delta = { x: target.x - current.x, y: target.y - current.y };
}

✅ Correct - Inline values or use scalar maths:

update(app) {
  const dx = target.x - current.x;
  const dy = target.y - current.y;
  const dist = math.dist(current.x, current.y, target.x, target.y);
}

4. Smart Styling Writes

Use the dom.setStyle helper to skip styling writes unless the property value changes. For bulk initialization inside create/install hooks, use dom.applyStyles.

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

// Only writes to DOM if value actually changed
dom.setStyle(el, 'width', `${size}px`);
dom.setStyle(el, 'borderColor', color);

// Bulk initialization in install() or create()
dom.applyStyles(el, {
  position: 'fixed',
  pointerEvents: 'none',
  zIndex: Layers.CURSOR
});

5. Prefer CSS GPU Transforms

Never mutate CPU-bound properties like `top/left` inside updates. Use dom.setTransform to center elements via translate3d, offloading rendering to the GPU.

// ❌ WRONG - CPU bound
el.style.left = x + 'px';
el.style.top = y + 'px';

// ✅ CORRECT - GPU bound
dom.setTransform(el, x, y); // translate3d + centered automatically

Plugin Composition Patterns

Stacking Compositions

Layer multiple single-purpose plugins to construct compound cursors. Differentiating priority ensures logic (Stick) feeds coordinates into rendering layers (Ring) correctly.

const app = new Supermouse();

// Order matters!
app.use(Stick({ padding: 15 }));      // Logic: calcs geometry, writes state.shape
app.use(Magnetic({ distance: 100 })); // Logic: adds attraction
app.use(Ring({ size: 30, color: '#fff' }));   // Visual: reads state.shape, morphs
app.use(Dot({ size: 8, color: '#000' }));     // Visual: renders inner dot
app.use(Trail({ length: 12 }));               // Visual: renders trail

Runtime Dynamic Control

Call disablePlugin() or enablePlugin() to toggle specific layers on routes or configurations.

const app = new Supermouse();
const magneticPlugin = Magnetic({ distance: 100 });

app.use(magneticPlugin);

// Later, disable on certain pages
document.addEventListener('navigate', (e) => {
  if (e.newPage === 'admin') {
    app.disablePlugin('magnetic');
  } else {
    app.enablePlugin('magnetic');
  }
});

Always write clean visual exits in lifecycle hooks:

definePlugin({
  name: 'my-visual',
  onEnable(app, el) {
    el.style.opacity = '1';
  },
  onDisable(app, el) {
    // Fade out instead of instantly hide
    el.style.transition = 'opacity 0.2s';
    el.style.opacity = '0';
  }
}, options);

Configuration Best Practices

Handle Diverse Option Values

Users might pass static numbers, raw strings, or dynamic getters. The normalize() utility compiles any option shape into a clean getter.

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

export const MyPlugin = (options) => {
  // Normalize accepts: number, string, () => value, or reactive getter
  const getSize = normalize(options.size, 20);      // default 20
  const getColor = normalize(options.color, '#fff'); // default #fff

  return definePlugin({
    update(app) {
      const size = getSize(app.state);    // Call to evaluate
      const color = getColor(app.state);
    }
  }, options);
};

Standard configurations:

MyPlugin({ size: 30 });                        // static
MyPlugin({ size: () => Math.random() * 50 }); // dynamic
MyPlugin({ size: (state) => state.velocity.x }); // reactive

Document Static Restrictions

Remember that options passed inside constructor calls are static. If users need to adjust size or color dynamically, register methods or expose dynamic getter callbacks in the options.

export interface MyPluginOptions {
  name?: string;
  isEnabled?: boolean;
  /** Size in pixels. Set at construction, not reactive. */
  size?: number;
  /** Color can be a function for dynamic values. */
  color?: ValueOrGetter<string>;
}

export const MyPlugin = (options) => {
  let userSize = options.size ?? 20;

  return definePlugin({
    update(app) {
      // Always read userSize, allow external changes
      const size = userSize;
    }
  }, options);
};

Common Plugin Patterns

Rotation Syncing

Rotate elements in visual hooks based on cursor travel direction using math.lerpAngle.

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

let currentRotation = 0;

update(app, el) {
  const targetRotation = app.state.angle;

  // Smooth rotation to prevent jitter
  const smoothing = 0.15;
  currentRotation = math.lerpAngle(currentRotation, targetRotation, smoothing);

  dom.setTransform(el, x, y, currentRotation);
}

Sleep Timeouts

Reset elements to default idle configurations when pointer velocities approach zero using performance timestamps.

let stopTime = performance.now();
let isMoving = false;

update(app) {
  const speed = math.dist(app.state.velocity.x, app.state.velocity.y);

  if (speed > 1) {
    isMoving = true;
    stopTime = performance.now();
  } else {
    const stopDuration = performance.now() - stopTime;
    if (stopDuration > 500) {  // After 500ms idle
      isMoving = false;
      // Reset visuals
    }
  }
}

Precise Element Centering

Align custom visuals directly on the active cursor using dom.setTransform.

const create = (app) => {
  const el = document.createElement('div');
  el.style.position = 'fixed';
  el.style.pointerEvents = 'none';
  app.container.appendChild(el);
  return el;
};

const update = (app, el) => {
  const { x, y } = app.state.smooth;
  // setTransform automatically centers with translate(-50%, -50%)
  dom.setTransform(el, x, y);
};

Debugging & Troubleshooting

Visual Tearing / Lag

Symptom: The cursor inner dot tracks perfectly, but outer follower layers jitter or drift.

Fix: Ensure logic-modifying plugins (e.g. Magnetics, Snappers) use a negative priority value so they run before visual layers read coordinates.

// ❌ WRONG - Priority causes visual sync mismatch
priority: 0,
update(app) { app.state.target.x += 5; }

// ✅ CORRECT - Modifies target before physics or visuals run
priority: -10,
update(app) { app.state.target.x += 5; }

Layout Stutters

Symptom: Scrolling or rendering skips frames on hover shifts.

Fix: Stop reading sizes inside frame callbacks. Use app.registerHoverTarget() and cache client boundaries only when the hover target changes.

// ❌ WRONG - Queries DOM rect every single frame
update(app) {
  const rect = el.getBoundingClientRect();
}

// ✅ CORRECT - Scrapes targeting class once, caches calculations
let cachedRect = null;
install(app) {
  app.registerHoverTarget('[data-my-attr]');
}
update(app) {
  if (app.state.hoverTarget !== lastTarget) {
    lastTarget = app.state.hoverTarget;
    cachedRect = dom.projectRect(lastTarget, app.container);
  }
}

Audit Configuration

Run the built-in diagnostic test inside the developer console:

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

// Run in browser console
doctor();

Advanced Patterns

Deduplicate Core Instantiations

Avoid mounting multiple identical visual plugins. Let one coordinator manage multiple variants to keep DOM trees light.

// ❌ WRONG - Creates new HTML element per plugin
app.use(Dot({ color: 'red' }));
app.use(Dot({ color: 'blue' })); // Wasteful, no deduplication

Stateful Toggling

Implement state cleanups in `onEnable` and `onDisable` hooks to prevent garbage coordinates carrying over on toggle events.

definePlugin({
  name: 'my-plugin',

  onEnable(app, el) {
    // Reset to initial state
    el.style.opacity = '1';
    el.style.transform = '';
    currentPos.x = 0;
    currentPos.y = 0;
  },

  onDisable(app, el) {
    // Clean visual exit (fade, reset)
    el.style.transition = 'opacity 0.3s ease';
    el.style.opacity = '0';
  }
}, options);

Responsive Damping shifts

Scale cursor footprint sizes or turn off visual tracking altogether on narrow viewports to avoid blocking click actions.

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

return definePlugin({
  create: (app) => {
    const el = dom.createCircle(20, 'white');

    // Resize on window change
    const handleResize = () => {
      const isMobile = window.innerWidth < 768;
      el.style.width = isMobile ? '12px' : '20px';
    };

    window.addEventListener('resize', handleResize);
    return el;
  },
  destroy() {
    window.removeEventListener('resize', handleResize);
  }
}, options);

Publishing & Community

Naming Convention

Keep scope clear: name scopes as supermouse-plugin-xyz or @scope/supermouse-xyz. Organise namespace organization under @supermousejs/* organization naming.

Package Meta definition

Define a meta.json package wrapper configuration:

{
  "name": "my-plugin",
  "description": "Short description of what it does",
  "author": "Your Name",
  "repository": "https://github.com/yourname/supermouse-plugin-xyz",
  "npm": "supermouse-plugin-xyz"
}

Performance Checklist

  • Logic plugins run on negative priority (-10 or lower)
  • No direct DOM layout queries inside the update loop
  • Style updates go through dom.setStyle
  • Render alignment uses GPU transform translation vectors
  • Movements are multiplied against delta timers or damp coefficients
  • No object array instantiations inside frame updates
  • Targets boundaries calculations are cached on target changes
  • Interaction reads fetch from pre-scraped state.interaction caches
  • Elements fade out smoothly when deactivated
  • Plugins use factory scopes to prevent memory leaks

Quick Reference: Common Compositions

Magnetic Snap Combos

Combine Magnetic snappers with circular visual elements to pull attention to clickable targets cleanly.

import { Supermouse } from '@supermousejs/core';
import { Magnetic } from '@supermousejs/magnetic';

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

app.use(
  Magnetic({
    strength: 0.2,
    radius: 160,
    easing: 'ease-out',
  })
);

Momentum Trails

Pair Trail plugins with physics damping variables to create organic movements that draw visual interest.

import { Supermouse } from '@supermousejs/core';
import { Trail } from '@supermousejs/trail';

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

app.use(
  Trail({
    length: 12,
    color: '#eab308',
    width: 8,
    decay: 0.25,
  })
);
supermouse
js
| Copyright © 2024-2026 Stud.io Inc.