Back to Machine Coding
Machine Coding
easy
mid

How would you build a real time cursor tracker component?

Capture pointer position on `pointermove` over a container; render a custom cursor element following with `transform: translate(x, y)` via rAF (not React state — avoid re-render storms). Handle enter/leave to show/hide; use pointer events for cross-input (mouse/pen/touch); throttle to rAF; cleanup listeners on unmount.

4 min read·~20 min to think through

A custom cursor / tracker is a small but instructive exercise: it tests rAF, transform, event cleanup, and the importance of not re-rendering on every mouse move.

The naive (bad) version

jsx
function BadTracker() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  return (
    <div onPointerMove={(e) => setPos({ x: e.clientX, y: e.clientY })}>
      <div style={{ transform: `translate(${pos.x}px, ${pos.y}px)` }} />
    </div>
  );
}

This re-renders the whole tree on every pixel of movement. Smooth for a toy, terrible in a real app.

The good version — refs + rAF + transform

jsx
function CursorTracker() {
  const containerRef = useRef(null);
  const cursorRef = useRef(null);
  const target = useRef({ x: 0, y: 0 });
  const rafId = useRef(null);

  useEffect(() => {
    const container = containerRef.current;
    const cursor = cursorRef.current;

    const update = () => {
      cursor.style.transform = `translate(${target.current.x}px, ${target.current.y}px)`;
      rafId.current = null;
    };

    const onMove = (e) => {
      target.current.x = e.clientX;
      target.current.y = e.clientY;
      if (rafId.current == null) rafId.current = requestAnimationFrame(update);
    };

    const onLeave = () => cursor.style.opacity = "0";
    const onEnter = () => cursor.style.opacity = "1";

    container.addEventListener("pointermove", onMove);
    container.addEventListener("pointerleave", onLeave);
    container.addEventListener("pointerenter", onEnter);

    return () => {
      container.removeEventListener("pointermove", onMove);
      container.removeEventListener("pointerleave", onLeave);
      container.removeEventListener("pointerenter", onEnter);
      if (rafId.current) cancelAnimationFrame(rafId.current);
    };
  }, []);

  return (
    <div ref={containerRef} style={{ position: "relative", height: "100vh", cursor: "none" }}>
      <div
        ref={cursorRef}
        style={{
          position: "fixed",
          top: 0, left: 0,
          width: 20, height: 20,
          borderRadius: "50%",
          background: "white",
          mixBlendMode: "difference",
          pointerEvents: "none",
          opacity: 0,
          willChange: "transform",
          transition: "opacity 0.2s",
        }}
      />
    </div>
  );
}

What's load-bearing here

Refs, not state

Mouse position changes 60+ times per second. Don't put it in React state — store in a ref and write directly to the DOM via style.transform. React isn't the right tool for per-frame updates.

rAF throttling

Coalesce many pointermove events into one DOM write per frame. The pattern: "schedule a frame if one isn't already scheduled; record latest target."

Transform, not top/left

transform: translate is a composite-only property — no layout, no paint. top/left would trigger layout each frame. (See [[critical-rendering-path-crp-how-the-browser-renders-a-page]].)

pointer events, not mouse

pointermove is the unified API for mouse, pen, and touch. mousemove misses touch; touchmove misses pen.

pointer-events: none

The cursor element shouldn't catch its own events — otherwise it intercepts clicks and breaks the page underneath.

Cleanup

removeEventListener and cancelAnimationFrame on unmount, or you leak listeners (memory + zombie rAF callbacks).

Polish

  • Lerp for smooth lag: x += (target.x - x) * 0.2 per frame, gives a trailing effect.
  • Magnetic on hover — detect pointerenter on interactive elements and snap.
  • Hide on touch — touch devices don't have a hover cursor; check pointerType === "touch".
  • Reduced motion — respect prefers-reduced-motion: reduce (skip lerp, skip the custom cursor entirely).

Interview framing

"Don't put mouse position in React state — it'd re-render on every pixel. Listen for pointermove (unified for mouse/pen/touch), store the target position in a ref, schedule one rAF if not already scheduled, write directly to the element's style.transform. Transform is composite-only, no layout. pointer-events: none on the cursor so it doesn't catch its own events. Clean up listeners and rAF on unmount. Polish: lerp for trailing motion, hide on touch, respect prefers-reduced-motion."

Follow-up questions

  • Why refs and direct DOM writes instead of React state?
  • Why pointer events instead of mouse events?
  • Why animate transform instead of top/left?
  • What does pointer-events: none do here?

Common mistakes

  • Mouse position in React state — re-render storm.
  • Animating top/left → layout per frame.
  • No rAF — DOM write per pointermove (often 60+ per second).
  • Forgetting pointer-events: none — cursor blocks underlying interactions.
  • No cleanup — leaks listeners and rAF.

Performance considerations

  • rAF caps writes to ~60/sec (or vsync). Transform-only avoids layout/paint. willChange: transform hints the compositor to promote. Avoid forcing layout (offsetTop reads) inside the move handler.

Edge cases

  • Touch devices — no hover cursor; hide it.
  • Scrolling while moving — clientX/Y is viewport-relative, fine.
  • Multiple pointers (multi-touch) — pointerId disambiguates.
  • prefers-reduced-motion users.

Real-world examples

  • Apple's vision.apple.com custom cursor.
  • Awwwards-style portfolio sites.
  • Cursor-following tooltips, hover effects.

Senior engineer discussion

Seniors instinctively reach for refs + rAF + transform for any per-frame visual update, know not to involve React state, respect reduced-motion, and reach for pointer events for cross-input.

Related questions