Setup — Dot + Ring
Two child elements with data-lerp — the dot follows instantly (1), the ring follows with smooth delay (0.15).
<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;
&__item {
position: fixed;
top: 0; left: 0;
width: 8px; height: 8px;
background: #25282b;
border-radius: 50%;
pointer-events: none;
margin-left: -4px;
margin-top: -4px;
transition: width .2s, height .2s,
margin .2s, opacity .15s;
}
&__artwork {
position: fixed;
top: 0; left: 0;
width: 40px; height: 40px;
border: 1.5px solid #25282b;
border-radius: 50%;
pointer-events: none;
margin-left: -20px;
margin-top: -20px;
transition: width .3s, height .3s,
margin .3s, border-color .3s,
opacity .2s, background-color .3s;
}
}import CustomCursor from '@andresclua/custom-cursor';
const cursor = new CustomCursor({
element: '.c--cursor-a',
hideTrueCursor: true,
focusElements: ['a', 'button'],
focusClass: 'c--cursor-a--is-active',
hiddenClass: 'c--cursor-a--is-hidden',
clickingClass: 'c--cursor-a--second',
});Basic Focus — Links & Buttons
Pass selectors to focusElements and a focusClass — the cursor changes color on hover automatically.
<a href="#">Link Card</a>
<button>Button</button>.c--cursor-a {
&--is-active {
.c--cursor-a__item {
background-color: #e60000;
}
.c--cursor-a__artwork {
border-color: #e60000;
}
}
}const cursor = new CustomCursor({
element: '.c--cursor-a',
focusElements: ['a', 'button'],
focusClass: 'c--cursor-a--is-active',
});Custom Focus Class — Grow
Pass an object with elements and a custom focusClass to apply a different cursor state per element group.
<div class="js--grow">Grow A</div>
<div class="js--grow">Grow B</div>.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(230, 0, 0, 0.6);
background-color: rgba(230, 0, 0, 0.08);
}
}
}const cursor = new CustomCursor({
element: '.c--cursor-a',
focusElements: [
'a',
'button',
{
elements: '.js--grow',
focusClass: 'c--cursor-a--third',
},
],
});Focus with Callbacks — Text
Use mouseenter / mouseleave callbacks to inject text from data-cursor-text directly into the cursor ring.
<div class="js--text"
data-cursor-text="View">
View Project
</div>.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);
}
}
}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 = '';
},
},
],
});Disable / Enable
Call cursor.disable() to hide the cursor and restore the native pointer. Call cursor.enable() to bring it back.
<button id="js--toggle">
Disable Cursor
</button>const btn = document.getElementById('js--toggle');
let disabled = false;
btn.addEventListener('click', () => {
disabled = !disabled;
if (disabled) {
cursor.disable();
btn.textContent = 'Enable Cursor';
} else {
cursor.enable();
btn.textContent = 'Disable Cursor';
}
});Update Options
Call cursor.update(newOptions) to merge new config at runtime — without destroying the instance or stopping the rAF loop.
<button id="js--update">
Toggle Large Cursor
</button>const btn = document.getElementById('js--update');
let large = false;
btn.addEventListener('click', () => {
large = !large;
cursor.update({
focusClass: large
? 'c--cursor-a--third'
: 'c--cursor-a--is-active',
});
btn.textContent = large
? 'Revert Cursor'
: 'Toggle Large Cursor';
});Generic Focus Elements
Any CSS selector works in focusElements — not just a and button. Pass '.js--focus' to target arbitrary elements.
<span class="js--focus">
Focusable Span
</span>
<div class="js--focus">
Focusable Div
</div>const cursor = new CustomCursor({
element: '.c--cursor-a',
focusElements: [
'a',
'button',
'.js--focus',
],
focusClass: 'c--cursor-a--is-active',
});Dynamic Content — Load More
After injecting new DOM nodes (AJAX, infinite scroll), call cursor.update({}) to re-evaluate all selectors and pick up the new elements.
<div id="js--grid">
<div class="js--dynamic"
data-cursor-text="01">Card 01</div>
<div class="js--dynamic"
data-cursor-text="02">Card 02</div>
</div>
<button id="js--load-more">
Load More
</button>let count = 2;
const grid = document.getElementById('js--grid');
document
.getElementById('js--load-more')
.addEventListener('click', () => {
for (let i = 0; i < 2; i++) {
count++;
const card = document.createElement('div');
card.className = 'js--dynamic';
card.dataset.cursorText =
String(count).padStart(2, '0');
card.textContent =
'Card ' + String(count).padStart(2, '0');
grid.appendChild(card);
}
// Re-evaluate selectors — picks up new nodes
cursor.update({});
});