v1.1.0

CUSTOM
CURSOR

A lightweight JS library for customizable animated cursors — dot, ring, lerp, callbacks.

VIEW ON NPM ♥ Sponsor
00

Setup — Dot + Ring

Two child elements with data-lerp — the dot follows instantly (1), the ring follows with smooth delay (0.15).

Hover me
<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',
});
01

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',
});
02

Custom Focus Class — Grow

Pass an object with elements and a custom focusClass to apply a different cursor state per element group.

Grow A
Grow B
<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',
    },
  ],
});
03

Focus with Callbacks — Text

Use mouseenter / mouseleave callbacks to inject text from data-cursor-text directly into the cursor ring.

View Project
Read Article
Play Video
<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 = '';
      },
    },
  ],
});
04

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';
  }
});
05

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';
});
06

Generic Focus Elements

Any CSS selector works in focusElements — not just a and button. Pass '.js--focus' to target arbitrary elements.

Focusable Span
Focusable Div
<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',
});
07

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.

Card 01
Card 02
<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({});
  });