StringCursor Module

Translate pointer motion into CSS-driven hover states. StringCursor observes desktop mouse movement, writes normalized coordinates to targets, and keeps custom cursor elements in sync with eased values.

Activation primer: Add string="cursor" to every element you want StringCursor to control. Supply string-id="your-id" when you need a stable handle for events, mirrors, or string-copy-from; otherwise StringTune generates one automatically.

HTML Attributes

Interactive Element Attributes

These attributes are applied to elements with string="cursor" — the interactive targets that respond to hover.

AttributeTypeDefaultControlsPractical notes
string-target-disablebooleanfalseOpt the element out of cursor interactions even when flagged with string="cursor"Use on wrappers that should not respond despite inheriting the attribute
string-target-style-disablebooleanfalseSkip writing --x/--y CSS variables to this elementIdeal when you consume only the emitted events
string-cursor-targetstring"default"ID(s) of cursor portal(s) to target (comma/pipe-separated, or * for all)Links this element to specific cursor portal(s)
string-target-classstring""Class added to the target element itself while hoveredApplies to the interactive element for self-styling
string-cursor-classstring""Class added to the cursor portal element(s) while the target is hoveredUseful for theme-aware pointer styling on the custom cursor
string-alignmentenum (start/center/end)centerHow the module normalizes pointer position across the elementcenter yields -1…1, start/end map to 0…1 ranges
string-lerpnumber0.2Lerp factor applied to element's coordinate smoothing (remapped to 0.05–0.65)Larger numbers feel tighter; tweak per element for variety

Interactive element CSS variables (written every frame unless string-target-style-disable="true"):

  • --x, --y — normalized coordinates based on string-alignment setting

Cursor Portal Attributes

These attributes are applied to cursor portal elements with [string-cursor] or [data-string-cursor] — the custom cursor visuals that follow the pointer.

AttributeTypeDefaultControlsPractical notes
string-cursorstring"default"Portal ID for targeting from interactive elementsUse unique IDs for different cursor styles
string-cursor-idstringAlternative attribute for explicit portal IDUse when string-cursor value serves another purpose
string-cursor-lerpnumber0.75Independent lerp factor for this portal's smoothing (remapped to 0.05–0.65)Override global cursor smoothing per portal

Cursor portal CSS variables (written every frame):

  • --x, --y — smoothed cursor position in pixels (relative to viewport origin)
  • --x-lerp, --y-lerp — normalized velocity (movement per 60fps frame)

Cursor portal auto-classes (managed by the module):

  • -showing — added immediately when any linked interactive element is hovered
  • -show — added after 1200ms delay for progressive visual states

Module Snapshot

  • Activation attribute: string="cursor"
  • Per-target CSS variables: --x, --y (normalized coordinates, written to element and its mirrors)
  • Cursor portal CSS variables: --x, --y (smoothed pixels), --x-lerp, --y-lerp (normalized velocity)
  • Event channels: cursor:start|move|pixel|end:<id>, plus the global cursor event
  • Pointer gating: module auto-disables on coarse pointers (touch/pen)
  • Lifecycle helpers: attaches ResizeObservers, MutationObservers, and navigation listeners (beforeunload, pagehide, visibilitychange) to ensure cleanup on SPAs
  • Portal system: supports multiple cursor portals with independent IDs and lerp factors

Basic Usage

import StringTune, { StringCursor } from '@fiddle-digital/string-tune';

const stringTune = StringTune.getInstance();
stringTune.use(StringCursor);
stringTune.start(60);
<!-- Cursor portal - receives smoothed cursor position -->
<div string-cursor class="custom-cursor">
  <div string-cursor-content class="cursor-dot"></div>
</div>

<!-- Interactive element - tracks hover and emits normalized coordinates -->
<button string="cursor" string-cursor-class="-hovering" string-lerp="0.35" class="cta">Hover me</button>
/* Interactive element receives normalized coordinates */
.cta {
  --tx: calc(var(--x, 0) * 12px);
  --ty: calc(var(--y, 0) * 12px);
  transform: translate(var(--tx), var(--ty));
  transition: box-shadow 0.2s ease;
}

/* Cursor portal receives pixel coordinates with velocity */
.custom-cursor {
  position: fixed;
  inset: 0 auto auto 0;
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  pointer-events: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.6);
  transform: translate3d(calc(var(--x, 0) * 1px), calc(var(--y, 0) * 1px), 0);
  opacity: 0;
  transition: opacity 0.3s ease;
}

/* Show cursor when hovering interactive elements */
.custom-cursor.-showing {
  opacity: 1;
}

/* Additional state after delay */
.custom-cursor.-show {
  background: rgba(255, 100, 100, 0.8);
}

How It Works

  • Hover capture: elements with string="cursor" register mouseenter/leave listeners when they become visible via viewport tracking; Safari navigation quirks are handled with multiple lifecycle listeners.
  • Center sampling: the module tracks element centers via CenterCache with ResizeObservers, allowing coordinates to be normalized even as layouts shift.
  • Frame-rate compensation: string-lerp passes through an adaptive curve and gets FPS-adjusted to maintain consistent easing regardless of refresh rate (60Hz, 120Hz, 240Hz, etc.).
  • CSS writes: unless disabled via string-target-style-disable, every frame updates --x/--y (normalized) on the element and all mirrors created with string-copy-from.
  • Cursor portals: elements with [string-cursor] receive smoothed pixel coordinates (--x, --y) plus normalized velocity (--x-lerp, --y-lerp) for custom cursor styling. Multiple portals can coexist with different IDs and lerp factors.
  • Portal targeting: use string-cursor-target to link interactive elements to specific cursor portals. Supports wildcards (*), multiple IDs (comma/pipe-separated), or defaults to first available portal.
  • Hover classes: automatically adds -showing class immediately on hover, then -show class after 1200ms delay for progressive visual states.

Event Signals

Channel patternPayloadFired whenCommon uses
cursor:start:<id>nullPointer begins moving over the element (above epsilon)Kick off sound cues, toggle hover classes
cursor:move:<id>{ x: number; y: number; } (normalized)Normalized coordinates change beyond epsilonDrive DOM transforms, shaders, or analytics samples
cursor:pixel:<id>{ x: number; y: number; } (smoothed pixels relative to element)Smoothed pixel offsets update beyond epsilonPosition tooltips, magnetic highlights
cursor:end:<id>nullPointer stops moving (settles below epsilon)Fade out hover embellishments
cursor{ x: number; y: number; stepX: number; stepY: number; }Global smoothed cursor position changesSync custom cursor portals, broadcast to state store

Event payload details:

  • cursor:move and cursor:pixel respect alignment setting (center = -1…1, start/end = 0…1)
  • cursor event provides global smoothed position in viewport pixels plus movement deltas
  • All events honor epsilon threshold (0.0005) to avoid unnecessary updates
// Track normalized coordinates for element effects
stringTune.on('cursor:move:cta', ({ x, y }) => {
  // x & y are normalized based on string-alignment setting
  // Use for subtle parallax, tilt, or shader uniforms
});

// Track pixel offsets for precise positioning
stringTune.on('cursor:pixel:cta', ({ x, y }) => {
  // x & y are smoothed pixel offsets from element's top-left
  // Perfect for tooltip positioning or magnetic effects
});

// Track global cursor for state management
stringTune.on('cursor', ({ x, y, stepX, stepY }) => {
  // x, y: smoothed viewport pixels
  // stepX, stepY: movement deltas (useful for velocity effects)
});

Cursor Portal System

The module supports multiple independent cursor portals, each with its own ID and configuration. This allows different cursor styles for different interaction contexts.

<!-- Default cursor portal -->
<div string-cursor class="cursor-default"></div>

<!-- Special cursor portal for CTA elements -->
<div string-cursor="cta" string-cursor-lerp="0.25" class="cursor-cta"></div>

<!-- Link elements to specific portals -->
<button string="cursor" string-cursor-target="cta" string-cursor-class="-active">Call to Action</button>

<!-- Target multiple portals at once -->
<a string="cursor" string-cursor-target="default,cta">Multi-target link</a>

<!-- Target all portals -->
<div string="cursor" string-cursor-target="*">Affects all cursors</div>

Portal ID resolution:

  • Reads from string-cursor, data-string-cursor, string-cursor-id, or data-string-cursor-id
  • Falls back to "default" if no ID specified
  • Each portal can have independent string-cursor-lerp for different smoothing

Portal CSS variables:

  • --x, --y: smoothed cursor position in pixels (relative to viewport)
  • --x-lerp, --y-lerp: normalized velocity (movement per 60fps frame)

Portal classes:

  • -showing: added immediately when any linked element is hovered
  • -show: added after 1200ms delay (useful for delayed animations)

Advanced Examples

Multi-cursor setup with different behaviors:

<div string-cursor class="cursor-normal"></div>
<div string-cursor="drag" string-cursor-lerp="0.5" class="cursor-drag"></div>

<div string="cursor" string-cursor-target="normal">Hover element</div>
<div string="cursor" string-cursor-target="drag" string-cursor-class="-dragging">Draggable element</div>
.cursor-drag {
  transform: translate3d(calc(var(--x) * 1px), calc(var(--y) * 1px), 0) scale(1.5);
}

.cursor-drag.-dragging {
  transform: translate3d(calc(var(--x) * 1px), calc(var(--y) * 1px), 0) scale(2);
}

Using velocity for motion blur:

.custom-cursor {
  transform: translate3d(calc(var(--x) * 1px), calc(var(--y) * 1px), 0);
  filter: blur(calc(abs(var(--x-lerp, 0)) * 2px + abs(var(--y-lerp, 0)) * 2px));
}

Element-level effects with normalized coordinates:

.card {
  transform: perspective(1000px) rotateY(calc(var(--x, 0) * 10deg)) rotateX(calc(var(--y, 0) * -10deg));
  transition: transform 0.1s ease-out;
}

Integration with Other Modules

StringCursor works seamlessly with other StringTune modules:

<!-- Combine with StringMirror for synchronized effects -->
<div string="cursor" string-id="card-main">
  <div string-copy-from="card-main" class="card-reflection"></div>
</div>

<!-- Use events with StringMagnetic for advanced interactions -->
<button string="cursor magnetic" string-lerp="0.3" string-magnetic-strength="0.8">Magnetic button</button>

Performance Notes

  • Auto-disable on touch: module detects coarse pointers and disables automatically on mobile/tablet
  • FPS compensation: lerp values are adjusted based on actual frame rate to maintain consistent feel across 60Hz, 120Hz, and 240Hz displays
  • Epsilon threshold: movement below 0.0005 is considered settled, preventing unnecessary updates
  • Safari navigation fixes: multiple lifecycle hooks ensure proper cleanup when navigating in SPAs
  • ResizeObserver: automatically tracks element size changes for accurate center calculations

StringCursor keeps hover choreography responsive—dial the lerp per component, wire events into motion systems, use multiple cursor portals for context-aware styling, and let frame-rate compensation handle the rest.