A performant cursor flashlight effect in React
A soft cursor glow is a nice touch, but only if it doesn’t tank your frame rate. Here’s how to build one that’s performant.
Cursor Flashlight
Move your cursor to reveal the glow.
The implementation
The trick is avoiding React re-renders entirely. We hold the DOM node in a ref and mutate its style directly, use GPU-composited transforms instead of layout-triggering properties, and batch updates with requestAnimationFrame.
import { useEffect, useRef } from 'react';
import styles from './CursorFlashlight.module.css';
export default function CursorFlashlight() {
const glowRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let frame = 0;
let x = 0;
let y = 0;
const render = () => {
frame = 0;
if (glowRef.current) {
glowRef.current.style.transform = `translate3d(${x - 250}px, ${y - 250}px, 0)`;
}
};
const handleMouseMove = (event: MouseEvent) => {
x = event.clientX;
y = event.clientY;
if (!frame) frame = requestAnimationFrame(render);
};
window.addEventListener('mousemove', handleMouseMove, { passive: true });
return () => {
window.removeEventListener('mousemove', handleMouseMove);
if (frame) cancelAnimationFrame(frame);
};
}, []);
return <div ref={glowRef} aria-hidden="true" className={styles.glow} />;
}
A simple CSS Module is used for styling the glow effect:
/* CursorFlashlight.module.css */
.glow {
position: fixed;
top: 0;
left: 0;
width: 500px;
height: 500px;
border-radius: 50%;
background: radial-gradient(
circle,
rgba(255, 255, 255, 0.18) 0%,
rgba(255, 255, 255, 0.08) 35%,
rgba(255, 255, 255, 0) 70%
);
pointer-events: none;
will-change: transform;
transform: translate3d(-9999px, -9999px, 0);
mix-blend-mode: screen;
z-index: 9999;
}
The details and why it’s fast
- Direct DOM manipulation bypassing React state means the component renders once and never again
translate3dthe GPU composites this without layout or paintrequestAnimationFrameguard theif (!frame)check ensures we update at most once per frame, even ifmousemovefires fasterwill-change: transformtells the browser to prepare a compositor layer up frontpassive: truelets the browser paint in parallel since we’re not blocking withpreventDefault()mix-blend-mode: screenbrightens instead of darkens, which works better on dark backgrounds- Initial position off-screen avoids a flash at
(0, 0)before the first mousemove