Blog / JavaScript

Custom Cursor: Magnetic, Trail and Spotlight

Create unique cursors that transform the user experience. 5 variations with interactive demos: dot + circle, trail effect, magnetic cursor, spotlight and blend mode.

Introduction

The custom cursor has become a signature element of modern web design. Sites like Apple, Stripe or Awwwards use it to create an immersive and memorable experience.

In this tutorial, we will explore 5 custom cursor variations, from simplest to most complex. Each demo is interactive - move your mouse in the designated areas to see the effect in action.

💡
Good to know

Custom cursors only work on desktop. On mobile, remember to provide a fallback or disable the effect.

1. Simple custom cursor (dot + circle)

The foundation of any custom cursor: a small central dot that follows the mouse exactly, surrounded by a larger circle with a slight delay (easing). It's elegant and unobtrusive.

Interactive demo - Move your mouse

Move your mouse in this area

cursor-simple.js
// HTML: Add these elements to your body
// <div class="cursor-dot"></div>
// <div class="cursor-circle"></div>

const dot = document.querySelector('.cursor-dot');
const circle = document.querySelector('.cursor-circle');

let mouseX = 0, mouseY = 0;
let circleX = 0, circleY = 0;

document.addEventListener('mousemove', (e) => {
    mouseX = e.clientX;
    mouseY = e.clientY;

    // Dot follows instantly
    dot.style.left = mouseX + 'px';
    dot.style.top = mouseY + 'px';
});

// Circle animation with easing
function animate() {
    const ease = 0.15;

    circleX += (mouseX - circleX) * ease;
    circleY += (mouseY - circleY) * ease;

    circle.style.left = circleX + 'px';
    circle.style.top = circleY + 'px';

    requestAnimationFrame(animate);
}
animate();
cursor-simple.css
/* Hide the native cursor */
body {
    cursor: none;
}

.cursor-dot {
    position: fixed;
    width: 8px;
    height: 8px;
    background: #6366f1;
    border-radius: 50%;
    pointer-events: none;
    z-index: 9999;
    transform: translate(-50%, -50%);
}

.cursor-circle {
    position: fixed;
    width: 40px;
    height: 40px;
    border: 2px solid rgba(99, 102, 241, 0.5);
    border-radius: 50%;
    pointer-events: none;
    z-index: 9998;
    transform: translate(-50%, -50%);
    transition: width 0.2s, height 0.2s, border-color 0.2s;
}

2. Trail effect

The trail effect creates a trail of particles that follow the cursor. Each particle fades out progressively, creating an elegant comet effect. Perfect for creative sites.

Interactive demo - Move your mouse

Move your mouse quickly to see the trail

cursor-trail.js
const trailLength = 20;
const trails = [];

// Create trail elements
for (let i = 0; i < trailLength; i++) {
    const trail = document.createElement('div');
    trail.className = 'trail-particle';
    trail.style.opacity = 1 - (i / trailLength);
    trail.style.width = (12 - (i * 0.5)) + 'px';
    trail.style.height = trail.style.width;
    document.body.appendChild(trail);
    trails.push({ el: trail, x: 0, y: 0 });
}

let mouseX = 0, mouseY = 0;

document.addEventListener('mousemove', (e) => {
    mouseX = e.clientX;
    mouseY = e.clientY;
});

function animateTrail() {
    let x = mouseX, y = mouseY;

    trails.forEach((trail, i) => {
        const nextX = x;
        const nextY = y;

        trail.el.style.left = trail.x + 'px';
        trail.el.style.top = trail.y + 'px';

        x = trail.x;
        y = trail.y;

        trail.x += (nextX - trail.x) * 0.35;
        trail.y += (nextY - trail.y) * 0.35;
    });

    requestAnimationFrame(animateTrail);
}
animateTrail();
cursor-trail.css
.trail-particle {
    position: fixed;
    background: linear-gradient(135deg, #6366f1, #8b5cf6);
    border-radius: 50%;
    pointer-events: none;
    z-index: 9999;
    transform: translate(-50%, -50%);
}

3. Magnetic cursor

The magnetic cursor is attracted to interactive elements (buttons, links). When the mouse approaches, the element seems "magnetized" toward the cursor. This is a technique widely used by creative agencies.

Interactive demo - Bring your mouse close to the button

Bring your mouse close without clicking

cursor-magnetic.js
const magneticElements = document.querySelectorAll('.magnetic');
const magnetStrength = 0.4; // Magnet strength

magneticElements.forEach(elem => {
    elem.addEventListener('mousemove', (e) => {
        const rect = elem.getBoundingClientRect();
        const centerX = rect.left + rect.width / 2;
        const centerY = rect.top + rect.height / 2;

        const deltaX = (e.clientX - centerX) * magnetStrength;
        const deltaY = (e.clientY - centerY) * magnetStrength;

        elem.style.transform =
            `translate(${deltaX}px, ${deltaY}px)`;
    });

    elem.addEventListener('mouseleave', () => {
        elem.style.transform = 'translate(0, 0)';
    });
});

// Version with expanded attraction zone
function initMagneticAdvanced(selector, options = {}) {
    const { strength = 0.3, range = 100 } = options;
    const elements = document.querySelectorAll(selector);

    document.addEventListener('mousemove', (e) => {
        elements.forEach(elem => {
            const rect = elem.getBoundingClientRect();
            const centerX = rect.left + rect.width / 2;
            const centerY = rect.top + rect.height / 2;

            const distance = Math.hypot(
                e.clientX - centerX,
                e.clientY - centerY
            );

            if (distance < range) {
                const factor = 1 - (distance / range);
                const deltaX = (e.clientX - centerX) * strength * factor;
                const deltaY = (e.clientY - centerY) * strength * factor;
                elem.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
            } else {
                elem.style.transform = 'translate(0, 0)';
            }
        });
    });
}

4. Spotlight / Torch effect

The spotlight effect reveals content as if the user were holding a flashlight. Ideal for mysterious landing pages or product reveals.

Interactive demo - Move your mouse to reveal

Hidden content

Move the light to discover this secret text!

cursor-spotlight.js
const spotlight = document.querySelector('.spotlight-overlay');
const container = document.querySelector('.spotlight-container');

container.addEventListener('mousemove', (e) => {
    const rect = container.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    spotlight.style.background = `
        radial-gradient(
            circle 150px at ${x}px ${y}px,
            transparent 0%,
            rgba(10, 10, 15, 0.95) 100%
        )
    `;
});

container.addEventListener('mouseleave', () => {
    spotlight.style.background = 'rgba(10, 10, 15, 0.95)';
});
cursor-spotlight.css
.spotlight-container {
    position: relative;
    overflow: hidden;
}

.spotlight-overlay {
    position: absolute;
    inset: 0;
    background: rgba(10, 10, 15, 0.95);
    pointer-events: none;
    transition: background 0.1s;
}

5. Cursor with blend mode

Blend mode allows the cursor to interact with the page's colors. With mix-blend-mode: difference, the cursor inverts the colors beneath it, creating a striking visual effect.

Interactive demo - The cursor inverts colors

Observe how the cursor adapts to the background

cursor-blend.js
const cursor = document.querySelector('.cursor-blend');
let cursorX = 0, cursorY = 0;
let targetX = 0, targetY = 0;

document.addEventListener('mousemove', (e) => {
    targetX = e.clientX;
    targetY = e.clientY;
});

function animateCursor() {
    cursorX += (targetX - cursorX) * 0.1;
    cursorY += (targetY - cursorY) * 0.1;

    cursor.style.left = cursorX + 'px';
    cursor.style.top = cursorY + 'px';

    requestAnimationFrame(animateCursor);
}
animateCursor();

// Enlarge on link hover
document.querySelectorAll('a, button').forEach(el => {
    el.addEventListener('mouseenter', () => {
        cursor.style.transform = 'translate(-50%, -50%) scale(2)';
    });
    el.addEventListener('mouseleave', () => {
        cursor.style.transform = 'translate(-50%, -50%) scale(1)';
    });
});
cursor-blend.css
.cursor-blend {
    position: fixed;
    width: 40px;
    height: 40px;
    background: #fff;
    border-radius: 50%;
    pointer-events: none;
    z-index: 9999;
    transform: translate(-50%, -50%);
    mix-blend-mode: difference;
    transition: transform 0.2s ease;
}

Best practices and accessibility

Custom cursors add style, but can harm the experience if poorly implemented. Here are the essential rules.

Performance

  • Use requestAnimationFrame instead of setInterval for smooth animations
  • Prefer transform over left/top for animations (GPU accelerated)
  • Limit the number of particles for the trail effect (15-25 max)
  • Use will-change: transform sparingly

Accessibility

  • Never completely hide the cursor over selectable text areas
  • Maintain sufficient contrast between the cursor and the background
  • Respect prefers-reduced-motion for animations
accessibility.css
/* Disable on mobile */
@media (hover: none) {
    .cursor-dot,
    .cursor-circle,
    .cursor-blend {
        display: none;
    }
    body {
        cursor: auto;
    }
}

/* Respect user preferences */
@media (prefers-reduced-motion: reduce) {
    .cursor-dot,
    .cursor-circle {
        transition: none;
    }
    .trail-particle {
        display: none;
    }
}
⚠️
UX Warning

A cursor that is too large or too slow can frustrate the user. Always test with real users and keep the effect subtle.

Conclusion

Custom cursors are an excellent way to differentiate your site and create a memorable experience. With these 5 techniques, you have a solid foundation to experiment with.

Start with the simple cursor (dot + circle), then explore more advanced variations depending on your project's style. And don't forget: subtlety is the key to a good cursor effect.

🎨
Go further

Find complete templates with integrated cursors in our effects library.