Lightweight custom cursor library — hover, click, and interact with the examples below.
A single cursor wrapper with two items — a dot that follows instantly and a ring that follows with smooth delay.
<div class="c--cursor-a">
<span class="c--cursor-a__item" data-lerp="1"></span>
<span class="c--cursor-a__artwork" data-lerp="0.15"></span>
</div>.c--cursor-a {
position: fixed;
top: 0;
left: 0;
pointer-events: none;
z-index: 10000;
&--is-hidden {
.c--cursor-a__item,
.c--cursor-a__artwork { opacity: 0; }
}
&--is-active {
.c--cursor-a__item { background-color: #e74c3c; }
.c--cursor-a__artwork { border-color: #e74c3c; }
}
&__item {
position: fixed;
top: 0;
left: 0;
border-radius: 50%;
pointer-events: none;
width: 8px;
height: 8px;
background: #111;
margin-left: -4px;
margin-top: -4px;
}
&__artwork {
position: fixed;
top: 0;
left: 0;
border-radius: 50%;
pointer-events: none;
width: 40px;
height: 40px;
border: 1.5px solid #111;
margin-left: -20px;
margin-top: -20px;
}
}import CustomCursor from '@andresclua/custom-cursor';
const cursor = new CustomCursor({
element: '.c--cursor-a',
hideTrueCursor: true,
disableTouch: true,
focusElements: [
'a',
'button',
'.js--focus',
{ elements: '.js--grow', focusClass: 'c--cursor-a--third' },
{
elements: '.js--text',
focusClass: 'c--cursor-a--fourth',
mouseenter(cursorEl, el) { /* ... */ },
mouseleave(cursorEl) { /* ... */ },
},
],
focusClass: 'c--cursor-a--is-active',
hiddenClass: 'c--cursor-a--is-hidden',
clickingClass: 'c--cursor-a--second',
onInit: null,
onDestroy: null,
onMove: null,
});
// #init() and #events() run automatically (private)Hover over links and buttons — the cursor changes color.
<a href="#" class="c--card-a">Link Card</a>
<button class="c--btn-a">Button</button>
<a href="#" class="c--card-a">Another Link</a>/* Dot turns red, ring border turns red */
.c--cursor-a {
&--is-active {
.c--cursor-a__item { background-color: #e74c3c; }
.c--cursor-a__artwork { border-color: #e74c3c; }
}
}// Already handled by focusElements in the constructor:
const cursor = new CustomCursor({
element: '.c--cursor-a',
focusElements: ['a', 'button'],
focusClass: 'c--cursor-a--is-active',
});Hover over these cards — the cursor grows.
<div class="c--card-a js--grow">Grow Card A</div>
<div class="c--card-a js--grow">Grow Card B</div>/* Dot hides, ring expands */
.c--cursor-a {
&--third {
.c--cursor-a__item { opacity: 0; }
.c--cursor-a__artwork {
width: 60px;
height: 60px;
margin-left: -30px;
margin-top: -30px;
border-color: rgba(52, 152, 219, 0.6);
background-color: rgba(52, 152, 219, 0.1);
}
}
}// Pass in the constructor focusElements array:
const cursor = new CustomCursor({
element: '.c--cursor-a',
focusElements: [
{ elements: '.js--grow', focusClass: 'c--cursor-a--third' },
],
});Hover over these cards — the cursor shows text from each card's data-cursor-text attribute.
<div class="c--card-a js--text" data-cursor-text="View">View Project</div>
<div class="c--card-a js--text" data-cursor-text="Read">Read Article</div>
<div class="c--card-a js--text" data-cursor-text="Play">Play Video</div>
<div class="c--card-a js--text" data-cursor-text="Explore">Explore Gallery</div>/* Dot hides, ring becomes filled circle with text */
.c--cursor-a {
&--fourth {
.c--cursor-a__item { opacity: 0; }
.c--cursor-a__artwork {
width: 80px;
height: 80px;
margin-left: -40px;
margin-top: -40px;
border-color: transparent;
background-color: rgba(0, 0, 0, 0.85);
}
}
}// Pass in the constructor focusElements array:
const cursor = new CustomCursor({
element: '.c--cursor-a',
focusElements: [
{
elements: '.js--text',
focusClass: 'c--cursor-a--fourth',
mouseenter(cursorEl, el) {
cursorEl.querySelector('.c--cursor-a__artwork').textContent =
el.dataset.cursorText || 'View';
},
mouseleave(cursorEl) {
cursorEl.querySelector('.c--cursor-a__artwork').textContent = '';
},
},
],
});The cursor shows a default label. Hover over cards to change the text; on leave it reverts to the default.
<section class="js--label-zone" data-cursor-text="Hi">
<div class="c--card-a js--label" data-cursor-text="View">View Project</div>
<div class="c--card-a js--label" data-cursor-text="Read">Read Article</div>
<div class="c--card-a js--label" data-cursor-text="Play">Play Video</div>
</section>/* Dot hides, ring becomes filled circle with persistent text */
.c--cursor-a {
&--fifth {
.c--cursor-a__item { opacity: 0; }
.c--cursor-a__artwork {
width: 80px;
height: 80px;
margin-left: -40px;
margin-top: -40px;
border-color: transparent;
background-color: rgba(44, 62, 80, 0.9);
}
}
}// Pass both zone and cards in the constructor focusElements array:
const cursor = new CustomCursor({
element: '.c--cursor-a',
focusElements: [
// 1. Zone: enter → show default text, leave → clear
{
elements: '.js--label-zone',
focusClass: 'c--cursor-a--fifth',
mouseenter(cursorEl, el) {
cursorEl.querySelector('.c--cursor-a__artwork').textContent =
el.dataset.cursorText || 'Hi';
},
mouseleave(cursorEl) {
cursorEl.querySelector('.c--cursor-a__artwork').textContent = '';
},
},
// 2. Cards: enter → swap text, leave → revert to zone default
{
elements: '.js--label',
focusClass: 'c--cursor-a--sixth',
mouseenter(cursorEl, el) {
cursorEl.querySelector('.c--cursor-a__artwork').textContent =
el.dataset.cursorText || 'View';
},
mouseleave(cursorEl, el) {
const zone = el.closest('.js--label-zone');
cursorEl.querySelector('.c--cursor-a__artwork').textContent =
zone ? zone.dataset.cursorText : 'Hi';
},
},
],
});Toggle the custom cursor on and off.
<button class="c--btn-a" id="js--toggle">Disable Cursor</button>const toggleBtn = document.getElementById('js--toggle');
let isDisabled = false;
toggleBtn.addEventListener('click', () => {
isDisabled = !isDisabled;
if (isDisabled) {
cursor.disable();
} else {
cursor.enable();
}
});Change cursor options at runtime.
<button class="c--btn-a" id="js--update">Toggle Large Cursor</button>const updateBtn = document.getElementById('js--update');
let isLarge = false;
updateBtn.addEventListener('click', () => {
isLarge = !isLarge;
if (isLarge) {
cursor.update({ focusClass: 'c--cursor-a--third' });
} else {
cursor.update({ focusClass: 'c--cursor-a--is-active' });
}
});Any element with .js--focus triggers the focused state.
<span class="js--focus">Focusable Span</span>
<div class="js--focus">Focusable Div</div>// Pass '.js--focus' in the constructor focusElements array:
const cursor = new CustomCursor({
element: '.c--cursor-a',
focusElements: ['a', 'button', '.js--focus'],
focusClass: 'c--cursor-a--is-active',
});When new HTML is injected into the DOM (AJAX, sliders, infinite scroll), the cursor doesn't know about the new elements. Call cursor.update({}) to re-bind — selectors are re-evaluated and new nodes are picked up automatically.
<div class="example__row" id="js--grid">
<div class="c--card-a js--dynamic" data-cursor-text="01">Card 01</div>
<div class="c--card-a js--dynamic" data-cursor-text="02">Card 02</div>
</div>
<button class="c--btn-a" id="js--load-more">Load More</button>/* Same text-mode styles from example 3 */
.c--cursor-a {
&--fourth {
.c--cursor-a__item { opacity: 0; }
.c--cursor-a__artwork {
width: 80px;
height: 80px;
margin-left: -40px;
margin-top: -40px;
border-color: transparent;
background-color: rgba(0, 0, 0, 0.85);
}
}
}// 1. Dynamic cards are registered via selector in the constructor:
const cursor = new CustomCursor({
element: '.c--cursor-a',
focusElements: [
{
elements: '.js--dynamic',
focusClass: 'c--cursor-a--fourth',
mouseenter(cursorEl, el) {
cursorEl.querySelector('.c--cursor-a__artwork').textContent =
el.dataset.cursorText || 'View';
},
mouseleave(cursorEl) {
cursorEl.querySelector('.c--cursor-a__artwork').textContent = '';
},
},
],
});
// 2. Simulate AJAX — inject new cards and call update()
const grid = document.getElementById('js--grid');
const loadMoreBtn = document.getElementById('js--load-more');
let cardCount = 2;
loadMoreBtn.addEventListener('click', () => {
for (let i = 0; i < 2; i++) {
cardCount++;
const card = document.createElement('div');
card.className = 'c--card-a js--dynamic';
card.dataset.cursorText = String(cardCount).padStart(2, '0');
card.textContent = 'Card ' + String(cardCount).padStart(2, '0');
grid.appendChild(card);
}
// 3. Key step: re-bind so new .js--dynamic elements are picked up
cursor.update({});
});