npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cursor-orb

v0.2.1

Published

A tiny, accessible, dependency-free cursor orb for modern websites.

Readme

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.