Halo Cursor is a lightweight, framework‑agnostic animated cursor halo for modern web apps. It follows the pointer, highlights interactive elements, and can completely hide the native cursor – all in plain TypeScript with zero runtime dependencies. (demo)
It works great with:
- Angular
- React
- Vue
- Svelte
- plain HTML / TypeScript / JavaScript
- Framework‑agnostic – Works in any app that can run JavaScript in the browser
- Animated halo – Smooth lerped motion with inner dot + outer halo
- Interactive‑aware – Highlights buttons/links and other interactive elements
- Motion‑safe – Respects
prefers-reduced-motionand coarse pointers (e.g. touch) - Cursor control – Optional full native cursor hiding with
hideNativeCursor - Scoped & controllable – Scope to a container with
rootElement, and usepause/resume - Tiny & dependency‑free – Just one class and sensible defaults
# npm
npm install @andreasnicolaou/halo-cursor
# yarn
yarn add @andreasnicolaou/halo-cursor
# pnpm
pnpm add @andreasnicolaou/halo-cursor<!-- unpkg CDN (latest version, unminified) -->
<script src="https://unpkg.com/@andreasnicolaou/halo-cursor/dist/index.umd.js"></script>
<!-- unpkg CDN (latest version, minified) -->
<script src="https://unpkg.com/@andreasnicolaou/halo-cursor/dist/index.umd.min.js"></script>
<!-- jsDelivr CDN (unminified) -->
<script src="https://cdn.jsdelivr.net/npm/@andreasnicolaou/halo-cursor/dist/index.umd.js"></script>
<!-- jsDelivr CDN (minified) -->
<script src="https://cdn.jsdelivr.net/npm/@andreasnicolaou/halo-cursor/dist/index.umd.min.js"></script>UMD (global halo variable):
<script src="https://unpkg.com/@andreasnicolaou/halo-cursor/dist/index.umd.min.js"></script>
<script>
// window.halo is the UMD global
const cursor = new halo.Cursor({
hideNativeCursor: true,
});
cursor.mount();
// later: cursor.destroy();
</script>import { Cursor } from '@andreasnicolaou/halo-cursor';
const cursor = new Cursor({
outerSize: 40,
innerSize: 8,
hideNativeCursor: true,
});
cursor.mount();const { Cursor } = require('@andreasnicolaou/halo-cursor');
const cursor = new Cursor({
hideNativeCursor: true,
});
cursor.mount();import { Cursor } from '@andreasnicolaou/halo-cursor';
const cursor = new Cursor({
color: '#6366f1',
hoverColor: '#818cf8',
outerSize: 36,
hoverOuterSize: 52,
hideNativeCursor: true,
lerp: 0.12,
});
// Attach to the whole document
cursor.mount();
// Temporarily pause animation/interaction (e.g. while a modal is open)
cursor.pause();
cursor.resume();
// Clean up when leaving the page / unmounting your app
// cursor.destroy();| Method | Description |
|---|---|
new Cursor(options?) |
Creates a new halo cursor instance with the given options. |
mount() |
Injects styles, creates DOM elements, attaches events, and starts the animation loop. |
destroy() |
Stops the loop, removes event listeners, DOM nodes, and restores the native cursor. |
updateOptions(options) |
Merges new options into the existing ones and reinjects the stylesheet while mounted. |
pause() |
Temporarily pauses tracking and hides the halo off‑screen without destroying the instance. |
resume() |
Resumes tracking and animation after a previous pause(). |
| Property | Type | Default | Description |
|---|---|---|---|
outerSize |
number |
36 |
Size of the outer halo ring in pixels |
innerSize |
number |
6 |
Size of the inner dot in pixels |
hoverOuterSize |
number |
52 |
Outer ring size on hover |
clickOuterSize |
number |
26 |
Outer ring size on click |
color |
string |
'#6366f1' |
Base color of the cursor halo |
hoverColor |
string |
'#818cf8' |
Color on hover state |
outerBorderColor |
string |
'rgba(99, 102, 241, 0.7)' |
Outer ring border color |
hoverBorderColor |
string |
'rgba(129, 140, 248, 1)' |
Outer ring border color on hover |
outerBackground |
string |
'transparent' |
Outer ring background fill |
hoverBackground |
string |
'rgba(99, 102, 241, 0.08)' |
Background fill on hover |
clickBackground |
string |
'rgba(99, 102, 241, 0.18)' |
Background fill on click |
zIndex |
number |
9999 |
CSS z-index for cursor elements |
lerp |
number |
0.12 |
Smoothing factor for cursor movement (0–1, clamped). Higher = snappier, lower = smoother/more lag |
hideNativeCursor |
boolean |
false |
Hide the native browser cursor |
interactiveSelectors |
string |
'a, button, [role="button"], input, textarea, select, label, [tabindex="0"]' |
CSS selectors for interactive elements |
classPrefix |
string |
'halo-cursor' |
CSS class name prefix for generated elements |
disableOnReducedMotion |
boolean |
true |
Disable animations if reduced motion is preferred |
rootElement |
HTMLElement | null |
null |
Root element to mount cursor into |
import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { Cursor } from '@andreasnicolaou/halo-cursor';
@Component({
selector: 'app-root',
template: ` <div #scope class="app-shell"><ng-content></ng-content></div> `,
})
export class AppComponent implements AfterViewInit, OnDestroy {
@ViewChild('scope', { static: true }) scopeRef!: ElementRef<HTMLDivElement>;
private cursor: Cursor | null = null;
ngAfterViewInit(): void {
this.cursor = new Cursor({ rootElement: this.scopeRef.nativeElement, hideNativeCursor: true });
this.cursor.mount();
}
ngOnDestroy(): void {
this.cursor?.destroy();
}
}import { useEffect, useRef } from 'react';
import { Cursor } from '@andreasnicolaou/halo-cursor';
export function AppCursorScope() {
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!ref.current) return;
const cursor = new Cursor({ rootElement: ref.current, hideNativeCursor: true });
cursor.mount();
return () => cursor.destroy();
}, []);
return <div ref={ref}>{/* your app */}</div>;
}import { onBeforeUnmount, onMounted, ref } from 'vue';
import { Cursor } from '@andreasnicolaou/halo-cursor';
const scopeRef = ref<HTMLElement | null>(null);
let cursor: Cursor | null = null;
onMounted(() => {
if (!scopeRef.value) return;
cursor = new Cursor({ rootElement: scopeRef.value, hideNativeCursor: true });
cursor.mount();
});
onBeforeUnmount(() => {
cursor?.destroy();
});Use in your template:
<template>
<div ref="scopeRef">
<!-- your content -->
</div>
</template>- The cursor auto‑disables itself on coarse pointers (e.g. touch devices).
- When
disableOnReducedMotionistrue(default), it also disables itself whenprefers-reduced-motion: reduceis enabled. - All DOM access happens inside
mount()/destroy(), keeping the class safe to instantiate in SSR environments.
Contributions, ideas, and bug reports are welcome. Feel free to open an issue or PR on the GitHub repository.