Skip to content

Instantly share code, notes, and snippets.

@khasky
Created April 8, 2026 06:37
Show Gist options
  • Select an option

  • Save khasky/7440628558f5d03ccbc17211463bdd07 to your computer and use it in GitHub Desktop.

Select an option

Save khasky/7440628558f5d03ccbc17211463bdd07 to your computer and use it in GitHub Desktop.
useCursorEffect React Hook
// On non-touch devices, adds a custom dual-layer cursor (ring + lagging dot with difference blend)
// that grows over links, buttons, and form controls.
import React from 'react';
export function useCursorEffect() {
React.useEffect(() => {
const isMobileDevice = () => {
return (
window.matchMedia('(pointer: coarse)').matches || /Mobi|Android/i.test(navigator.userAgent)
);
};
if (isMobileDevice()) {
return;
}
const cursor = document.createElement('div');
cursor.style.position = 'fixed';
cursor.style.width = '32px'; // 2rem
cursor.style.height = '32px'; // 2rem
cursor.style.borderRadius = '50%';
cursor.style.border = '2px solid #0080ff';
cursor.style.pointerEvents = 'none';
cursor.style.zIndex = '10000';
cursor.style.transition = 'transform 0.2s, border-color 0.2s';
cursor.style.transform = 'translate(-50%, -50%)';
cursor.style.mixBlendMode = 'difference';
document.body.appendChild(cursor);
const dot = document.createElement('div');
dot.style.position = 'fixed';
dot.style.width = '8px'; // 0.5rem
dot.style.height = '8px'; // 0.5rem
dot.style.backgroundColor = '#0080ff';
dot.style.borderRadius = '50%';
dot.style.pointerEvents = 'none';
dot.style.zIndex = '50';
dot.style.transition = 'transform 0.15s';
// dot.style.display = 'none'; // Hidden by default
dot.style.transform = 'translate(-50%, -50%)';
dot.style.mixBlendMode = 'difference';
document.body.appendChild(dot);
const moveCursor = (e: MouseEvent) => {
cursor.style.left = `${e.clientX}px`;
cursor.style.top = `${e.clientY}px`;
// Dot follows with delay
setTimeout(() => {
dot.style.left = `${e.clientX}px`;
dot.style.top = `${e.clientY}px`;
}, 50);
};
const expandCursor = () => {
cursor.style.transform = 'translate(-50%, -50%) scale(1.5)';
cursor.style.borderColor = 'white';
dot.style.transform = 'translate(-50%, -50%) scale(0)';
};
const resetCursor = () => {
cursor.style.transform = 'translate(-50%, -50%) scale(1)';
cursor.style.borderColor = '#0080ff';
dot.style.transform = 'translate(-50%, -50%) scale(1)';
};
document.addEventListener('mousemove', moveCursor);
const interactiveElements = document.querySelectorAll(
'a, button, input, textarea, [role="button"]'
);
interactiveElements.forEach((el) => {
el.addEventListener('mouseenter', expandCursor);
el.addEventListener('mouseleave', resetCursor);
});
return () => {
document.removeEventListener('mousemove', moveCursor);
interactiveElements.forEach((el) => {
el.removeEventListener('mouseenter', expandCursor);
el.removeEventListener('mouseleave', resetCursor);
});
document.body.removeChild(cursor);
document.body.removeChild(dot);
};
}, []);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment