Custom Cursor that Rotates with Movement!

Hi everybody - I added a new feature to the site where your default mouse cursor is replaced with a custom one that rotates based on the direction of the mouse movement!

Here is the full code :slight_smile:

SVG

<svg id="customCursor" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
    <!-- 
    This SVG defines a custom cursor with a unique design:
    - White and black two-tone color scheme
    - Drop shadow for depth
    - Pointer-like shape with a slight angle 
    -->
    <g filter="url(#filter0_d_2_333)">
        <!-- Main white pointer body -->
        <path d="M15.9231 18.0296C16.0985 18.4505 15.9299 20.0447 15 20.4142C14.0701 20.7837 12.882 20.4142 12.882 20.4142L10.726 16.1024L7 19.8284V3L18.4142 14.4142H14.1615C14.3702 14.8144 15.7003 17.4948 15.9231 18.0296Z" fill="white" style="fill:white;fill-opacity:1;"/>
        
        <!-- Black accent path for contrast and detail -->
        <path fill-rule="evenodd" clip-rule="evenodd" d="M8 5.41422V17.4142L11 14.4142L13.5 19.4142C13.5 19.4142 14.1763 19.63 14.5 19.4142C14.8237 19.1984 15.1457 18.7638 15 18.4142C14.3123 16.7638 12.5 13.4142 12.5 13.4142H16L8 5.41422Z" fill="black" style="fill:black;fill-opacity:1;"/>
    </g>
    
    <!-- 
    SVG Filter Definition for Drop Shadow:
    - Adds a subtle shadow effect to the cursor
    - Provides depth and visual separation from the background 
    -->
    <defs>
        <filter id="filter0_d_2_333" x="5.2" y="2.2" width="15.0142" height="21.1784" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
            <!-- Shadow configuration with gaussian blur and opacity -->
            <feFlood flood-opacity="0" result="BackgroundImageFix"/>
            <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
            <feOffset dy="1"/>
            <feGaussianBlur stdDeviation="0.9"/>
            <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.65 0"/>
            <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2_333"/>
            <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2_333" result="shape"/>
        </filter>
    </defs>
</svg>

CSS

body {
    /* Removes the default cursor for the entire body */
    cursor: none;
}

/* 
Restore default cursors for interactive elements 
Ensures usability for buttons, inputs, and other interactive components 
*/
button, input, textarea, select, [role="button"], p, li, ul, pre, code {
    cursor: auto !important;
}

a {
    /* Explicitly set pointer cursor for links */
    cursor: pointer !important;
}

#customCursor {
    /* Positioning and behavior of the custom cursor */
    position: fixed;      /* Positioned relative to the viewport */
    pointer-events: none; /* Ensures cursor doesn't interfere with interactions */
    width: 30px;
    height: 30px;
    transform-origin: center;
    left: 0;
    top: 0;
    z-index: 9999;        /* Ensures cursor is always on top */
    will-change: transform; /* Performance optimization hint */
    opacity: 1;
    transition: opacity 0.2s ease;
    display: none;        /* Initially hidden, shown by JavaScript */
}

#customCursor.hidden {
    /* Used to fade out cursor over interactive elements */
    opacity: 0;
}

JavaScript

// Get the custom cursor SVG element
const cursor = document.getElementById('customCursor');

// Position tracking variables
let lastX = 0;
let lastY = 0;
let targetX = 0;
let targetY = 0;
let currentX = 0;
let currentY = 0;

// Rotation tracking variables
let currentRotation = 0;
let targetRotation = 0;

// Animation and movement constants
const SMOOTHING = 0.2;        // Determines cursor movement smoothness
const MOVEMENT_THRESHOLD = 0.1; // Minimum movement to trigger rotation
const MAX_ROTATION_SPEED = 10;  // Limits rotation speed

// Linear interpolation function for smooth transitions
function lerp(start, end, factor) {
    // Calculates a point between start and end based on the factor
    return start + (end - start) * factor;
}

// Calculate the smallest angle between two rotations
function angleDifference(angle1, angle2) {
    // Ensures rotation takes the shortest path
    const diff = ((angle2 - angle1 + 180) % 360) - 180;
    return diff < -180 ? diff + 360 : diff;
}

// Update cursor position and visibility on mouse movement
function updateCursor(e) {
    // Set target coordinates to mouse position
    targetX = e.clientX;
    targetY = e.clientY;

    // Detect if the mouse is over an interactive element
    const targetElement = e.target;
    const isInteractive = targetElement.matches(
        'a, button, input, textarea, select, [role="button"], ' +
        '[contenteditable="true"], span, p, li, ul, pre, code, ' +
        'iframe, lite-youtube, img'
    );
    
    // Toggle cursor visibility and opacity
    cursor.classList.toggle('hidden', isInteractive);
    cursor.style.display = "block";
    
    // Position the cursor exactly at the mouse coordinates
    cursor.style.translate = `${targetX}px ${targetY}px`;
}

// Animate cursor movement and rotation
function animate() {
    // Smoothly interpolate cursor position
    currentX = lerp(currentX, targetX - 10, SMOOTHING);
    currentY = lerp(currentY, targetY - 10, SMOOTHING);
    
    // Calculate movement magnitude
    const deltaX = targetX - lastX;
    const deltaY = targetY - lastY;
    const movement = Math.sqrt(deltaX * deltaX + deltaY * deltaY);

    // Rotate cursor based on movement direction
    if (movement > MOVEMENT_THRESHOLD) {
        // Calculate angle of movement
        const angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI + 90;
        targetRotation = angle;
        
        // Calculate rotation difference
        const rotationDiff = angleDifference(currentRotation, targetRotation);
        
        // Limit rotation speed
        const rotationDelta = Math.min(
            Math.abs(rotationDiff),
            MAX_ROTATION_SPEED
        ) * Math.sign(rotationDiff);
        
        // Update current rotation
        currentRotation += rotationDelta;
        currentRotation = ((currentRotation + 180) % 360) - 180;
    }

    // Apply rotation to cursor
    cursor.style.rotate = `${currentRotation}deg`;

    // Update last known position
    lastX = targetX;
    lastY = targetY;

    // Continue animation loop
    requestAnimationFrame(animate);
}

// Start animation loop
requestAnimationFrame(animate);

// Add event listener for mouse movement
document.addEventListener('mousemove', updateCursor);

One of the things I played around with is the acceleration of the mouse cursor. I had the movement lag slightly with the actual mouse position, but that felt awkward. In this implementation, the only part is animated/accelerated is the cursor rotation only. The X/Y position moves in real-time with the actual cursor.

Hope you all like this effect :slight_smile:

Cheers,
Kirupa

1 Like