cursor-orb
v0.2.1
Published
A tiny, accessible, dependency-free cursor orb for modern websites.
Maintainers
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.
Features
- Smooth
requestAnimationFramepointer 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-orbnpm install cursor-orbyarn add cursor-orbCDN
<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
#6459d7border - transparent fill
0.15movement speed1.5automatic 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;
trueto use its defaults;falseto 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
tabindexvalues; - 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
hiddenThis 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:docsFor 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.
