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
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
Cheers,
Kirupa