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
  • translate3d the GPU composites this without layout or paint
  • requestAnimationFrame guard the if (!frame) check ensures we update at most once per frame, even if mousemove fires faster
  • will-change: transform tells the browser to prepare a compositor layer up front
  • passive: true lets the browser paint in parallel since we’re not blocking with preventDefault()
  • mix-blend-mode: screen brightens instead of darkens, which works better on dark backgrounds
  • Initial position off-screen avoids a flash at (0, 0) before the first mousemove

Did you enjoy this post?