Skip to content

Repository files navigation

cursor-orb

A small, dependency-free TypeScript library for an animated cursor orb with hover feedback, velocity stretch, click effects, magnetic movement, and contextual labels.

Live demo

Features

  • Smooth requestAnimationFrame pointer following
  • Automatic hover feedback for common interactive elements
  • Velocity-based motion stretch
  • Click pulse and expanding ripple
  • Optional magnetic movement with no layout measurements while disabled
  • Labels from data-cursor-orb-label
  • Hidden zones through data-cursor-orb-hidden
  • CSS custom properties and DOM state attributes
  • Fine-pointer and reduced-motion safeguards
  • Runtime updates and complete lifecycle cleanup
  • ESM, CommonJS, and TypeScript declarations
  • Zero runtime dependencies

Installation

pnpm add cursor-orb
npm install cursor-orb
yarn add cursor-orb

CDN

<script type="module">
  import CursorOrb from "https://cdn.jsdelivr.net/npm/cursor-orb/dist/index.js";

  const orb = new CursorOrb();
</script>

Quick start

import CursorOrb from "cursor-orb";

const orb = new CursorOrb();

The default instance uses:

  • 16 px diameter
  • 1 px solid #6459d7 border
  • transparent fill
  • 0.15 movement speed
  • 1.5 automatic interactive hover scale
  • click pulse and ripple
  • velocity stretch
  • labels when a labelled element is present
  • magnetic movement disabled

The orb starts hidden and becomes visible after the first pointer movement.

Configuration

Simple appearance and lifecycle values remain top-level. Related behavior is grouped into hover, click, motionStretch, magnetic, and label.

import { CursorOrb } from "cursor-orb";

const orb = new CursorOrb({
  size: 18,
  borderWidth: 2,
  borderColor: "#6459d7",
  borderStyle: "solid",
  fillColor: "rgba(100, 89, 215, 0.12)",
  opacity: 0.95,
  speed: 0.15,
  zIndex: 10000,
  transitionDuration: 180,

  hover: {
    scale: 1.6,
  },

  click: {
    pulse: true,
    duration: 480,
    ripple: true,
    rippleColor: "#6459d7",
    rippleScale: 3,
    rippleDuration: 620,
  },

  motionStretch: {
    strength: 0.6,
    maxScale: 1.65,
  },

  magnetic: {
    strength: 0.18,
  },

  label: {
    fontSize: 12,
    color: "#ffffff",
    fontFamily: "inherit",
    fontWeight: 600,
    padding: 8,
    maxWidth: 180,
    fillColor: "#6459d7",
    borderColor: "#6459d7",
  },
});

Every feature group accepts:

  • an object to configure it;
  • true to use its defaults;
  • false to disable it completely.
const orb = new CursorOrb({
  click: false,
  motionStretch: true,
  magnetic: false,
  label: false,
});

Base options

Option Type Default Description
size number 16 Orb diameter in CSS pixels
borderWidth number 1 Border width in CSS pixels
borderColor string #6459d7 Any CSS border color
borderStyle string solid CSS border style
fillColor string transparent Any CSS fill color
opacity number 1 Visible opacity from 0 to 1
speed number 0.15 Per-frame interpolation factor greater than 0 and at most 1
zIndex number 9999 Stacking level
transitionDuration number 180 Opacity, hover, size, and fill transition duration
className string "" Additional classes for the orb
interactiveSelector string "" Additional selector for project-specific interactive elements
respectFinePointer boolean true Require (pointer: fine)
respectReducedMotion boolean true Disable for reduced-motion users
reducedMotionBehavior "disable" | "static" "disable" Disable the orb or use instant, non-animated movement when reduced motion is requested
hideNativeCursor boolean false Hide the document's native cursor while mounted
autoStart boolean true Start during construction; constructor-only
allowMultipleInstances boolean false Permit multiple instances for one document; constructor-only
style Partial<CSSStyleDeclaration> {} Additional visual styles
document Document global document Document used for iframes or isolated DOM contexts

Hover

Hover feedback is automatic. No selector configuration is required.

CursorOrb recognizes:

  • links;
  • enabled buttons and form controls;
  • summary;
  • elements with link or button roles;
  • non-negative tabindex values;
  • draggable elements;
  • editable elements;
  • elements carrying data-cursor-orb-label.

Custom controls can be included with interactiveSelector:

const orb = new CursorOrb({
  interactiveSelector: "[data-action], .drag-handle",
});
const orb = new CursorOrb({
  hover: {
    scale: 1.8,
  },
});
Hover option Default Description
scale 1.5 Orb scale over an interactive element

Disable hover scaling:

orb.update({ hover: false });

Hidden zones

Add data-cursor-orb-hidden to an element or any container:

<div data-cursor-orb-hidden>
  The orb is hidden over this entire subtree.
</div>

The orb is also hidden over iframe and video elements by default. A hidden zone takes priority over hover, labels, magnetic movement, and click effects.

Click effects

Click feedback contains two independent effects:

  • a pulse on the main orb;
  • an expanding ripple at the exact pointer position.
const orb = new CursorOrb({
  click: {
    pulse: true,
    duration: 480,
    ripple: true,
    rippleColor: "#6459d7",
    rippleScale: 3,
    rippleDuration: 620,
  },
});
Click option Default Description
pulse true Contract, expand, and settle the main orb
duration 480 Pulse duration in milliseconds
ripple true Create an expanding click ring
rippleColor #6459d7 Ripple border color
rippleScale 3 Final ripple scale
rippleDuration 620 Ripple duration in milliseconds

Ripple elements are temporary, have pointer-events: none, and are removed when their animation finishes. destroy() also removes any active ripple immediately.

Motion stretch

Motion stretch deforms the orb along the current movement direction. The deformation responds to pointer velocity and smoothly settles back to a circle.

const orb = new CursorOrb({
  motionStretch: {
    strength: 0.8,
    maxScale: 1.9,
  },
});
Motion stretch option Default Description
strength 0.6 Velocity response; 0 produces no deformation
maxScale 1.65 Maximum horizontal stretch from 1 to 4

When motionStretch is false, velocity and deformation calculations are skipped.

Magnetic movement

Magnetic movement pulls the orb toward the center of the current interactive element:

const orb = new CursorOrb({
  magnetic: {
    strength: 0.2,
  },
});
Magnetic option Default Description
strength 0.18 Attraction from 0 to 1

Magnetic movement is disabled by default and stays aligned during scrolling and layout changes when enabled.

Cursor labels

Label text belongs to the markup:

<a href="/projects" data-cursor-orb-label="View">
  Projects
</a>

<button type="button" data-cursor-orb-label="Save">
  Save changes
</button>

Its appearance belongs to the CursorOrb configuration:

const orb = new CursorOrb({
  label: {
    fontSize: 13,
    color: "#ffffff",
    fontFamily: "Inter, sans-serif",
    fontWeight: 650,
    padding: 10,
    maxWidth: 200,
    fillColor: "#6459d7",
    borderColor: "#6459d7",
  },
});
Label option Default Description
fontSize 12 Text size in CSS pixels
color #ffffff Text color
fontFamily inherit Text font family
fontWeight 600 Text font weight
padding 8 Horizontal and vertical padding
maxWidth 180 Maximum label width
fillColor #6459d7 Orb fill while the label is visible
borderColor #6459d7 Orb border while the label is visible

Label text is inserted with textContent; markup from the attribute is never interpreted as HTML. While a label is visible, hover scaling and motion stretch are suspended so text remains stable and sharp. Labels are decorative, so the underlying element should retain an accessible name.

CSS custom properties

CursorOrb exposes its configuration and live motion state on [data-cursor-orb]:

Property Example Meaning
--cursor-orb-size 16px Base diameter
--cursor-orb-border-width 1px Border width
--cursor-orb-border-color #6459d7 Border color
--cursor-orb-border-style solid Border style
--cursor-orb-fill-color transparent Base fill
--cursor-orb-opacity 1 Visible opacity
--cursor-orb-hover-scale 1.5 Configured hover scale
--cursor-orb-stretch 1 Current live stretch
--cursor-orb-angle 0deg Current movement angle
--cursor-orb-stretch-strength 0.6 Configured stretch strength
--cursor-orb-magnetic-strength 0 Configured magnetic strength
--cursor-orb-ripple-color #6459d7 Configured ripple color
--cursor-orb-label-font-size 12px Configured label size
--cursor-orb-label-color #ffffff Configured label color
--cursor-orb-label-fill-color #6459d7 Configured label fill

The visual element also exposes its current state:

<div data-cursor-orb data-cursor-orb-state="hover"></div>

Possible values are:

visible
hover
label
hidden

This makes additional project-specific CSS possible without adding more JavaScript options:

[data-cursor-orb][data-cursor-orb-state="label"] {
  box-shadow: 0 8px 24px rgb(100 89 215 / 25%);
}

Runtime updates

orb.update({
  speed: 0.1,
  motionStretch: {
    strength: 0.9,
  },
  click: {
    rippleColor: "#22c55e",
  },
});

Feature objects are deeply merged with their existing values. Updating click.rippleColor, for example, preserves the existing pulse and duration settings.

Invalid options throw a descriptive RangeError or TypeError.

API

element

The visual HTMLDivElement, or null while CursorOrb is inactive:

orb.element?.classList.add("project-cursor");

active

Whether the instance is currently mounted.

mounted

Alias for active.

started

Whether the instance has been started.

update(options)

Updates appearance and behavior in place.

stop()

Removes the orb, active frames, effects, and listeners. The instance can be restarted.

start()

Restarts a stopped instance and reevaluates pointer and motion preferences.

destroy()

Permanently cleans up the instance. A destroyed instance cannot be restarted.

DOM structure

CursorOrb separates position, deformation, and visual effects so transforms cannot interfere with each other:

<div data-cursor-orb-positioner aria-hidden="true">
  <div data-cursor-orb-deformer>
    <div class="cursor-orb" data-cursor-orb aria-hidden="true">
      <span data-cursor-orb-label-text></span>
    </div>
  </div>
</div>

An active click ripple temporarily uses:

<div data-cursor-orb-ripple aria-hidden="true"></div>

Accessibility and device behavior

CursorOrb is decorative and keeps the native cursor visible by default. Set hideNativeCursor: true to use the orb as a visual replacement.

By default it is not mounted when:

  • (pointer: fine) does not match;
  • (prefers-reduced-motion: reduce) matches.

These preferences are observed while the instance is running.

Use reducedMotionBehavior: "static" to keep an instant, non-animated orb when reduced motion is requested.

Framework usage

Construct CursorOrb only on the client and destroy it during cleanup.

React

import { useEffect } from "react";
import CursorOrb from "cursor-orb";

export function App() {
  useEffect(() => {
    const orb = new CursorOrb();
    return () => orb.destroy();
  }, []);

  return <main>...</main>;
}

Vue

import { onMounted, onUnmounted } from "vue";
import CursorOrb from "cursor-orb";

let orb: CursorOrb | undefined;

onMounted(() => {
  orb = new CursorOrb();
});

onUnmounted(() => {
  orb?.destroy();
});

Development

pnpm install
pnpm dev
pnpm typecheck
pnpm test
pnpm build
pnpm build:docs

For Netlify, deploy from the repository root. The included netlify.toml builds the demo and publishes docs-dist; the source docs directory must not be deployed directly.

Browser support

CursorOrb targets modern browsers with matchMedia, Pointer Events, requestAnimationFrame, CSS custom properties, and the Web Animations API.

About

A small, dependency-free TypeScript library for an animated cursor orb with hover feedback, velocity stretch, click effects, magnetic movement, and contextual labels.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages