Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/apursley2012/dev-haunt/llms.txt

Use this file to discover all available pages before exploring further.

CursorTrail hides the operating system cursor and replaces it with a custom interactive effect: a short trail of glowing pumpkin-orange dots that spring into position behind the mouse pointer and fade out within half a second. Each dot is animated independently by Framer Motion — scaling down and fading to transparent as it ages — so the trail has a comet-like taper. A secondary ghost-coloured ring tracks the very tip of the cursor for precision reference. The component is mounted at the top of Layout and covers the entire viewport, but it never blocks interaction thanks to pointer-events: none.

Hiding the default cursor

CursorTrail only works correctly when the browser cursor is hidden. This is handled globally in main.css:
body {
  cursor: none;
}

a, button, [role=button], input, textarea {
  cursor: pointer;
}
The cursor: pointer overrides restore visible feedback on interactive elements — important for accessibility, since users still need a pointer visual on hover even though the OS arrow cursor is gone.
If you remove CursorTrail from Layout, also remove the cursor: none rule from main.css. Leaving it in place will hide the cursor with no replacement, making the site difficult to use.

Mouse tracking

A mousemove event listener is attached to window inside a useEffect:
useEffect(() => {
  let id = 0;
  const handleMouseMove = (e) => {
    const point = { x: e.clientX, y: e.clientY, id: id++ };
    setTrail(prev => [...prev.slice(-15), point]);
  };

  window.addEventListener('mousemove', handleMouseMove);
  return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
The state array is capped at the last 15 positions using .slice(-15). Older positions are pushed off the back as the mouse moves, giving the trail a consistent maximum length. A second useEffect clears the trail array 500 ms after the mouse stops moving:
useEffect(() => {
  if (trail.length === 0) return;
  const timer = setTimeout(() => setTrail([]), 500);
  return () => clearTimeout(timer);
}, [trail]);
This causes the dots to fade out and disappear when the cursor is idle rather than lingering indefinitely.

Trail dots

Each position in the state array is rendered as a <motion.div> wrapped in AnimatePresence:
<AnimatePresence>
  {trail.map(point => (
    <motion.div
      key={point.id}
      initial={{ opacity: 0.8, scale: 1 }}
      animate={{ opacity: 0, scale: 0.2 }}
      exit={{ opacity: 0 }}
      transition={{ duration: 0.5, ease: 'easeOut' }}
      className="absolute w-3 h-3 bg-pumpkin rounded-full"
      style={{
        left: point.x - 6,
        top: point.y - 6,
        boxShadow: '0 0 10px #ff7a1a, 0 0 20px #ff7a1a',
        borderRadius: '50% 50% 50% 50% / 60% 60% 40% 40%',
      }}
    />
  ))}
</AnimatePresence>
  • Colourbg-pumpkin (#ff7a1a) with a double-layer box-shadow glow in the same orange
  • Shape — slightly teardrop-shaped via an asymmetric border-radius (60% 60% 40% 40% on the vertical axis)
  • Fadeopacity: 0.8 → 0 over 0.5 s with easeOut
  • Shrinkscale: 1 → 0.2 over the same duration, making older dots visually recede
The oldest dot in the array is always the most faded and smallest, creating the taper effect naturally without any explicit ordering logic.

Cursor ring

In addition to the trail, a static ghost-coloured ring is rendered at the most recent cursor position:
{trail.length > 0 && (
  <div
    className="absolute w-4 h-4 border-2 border-ghost rounded-full pointer-events-none transition-transform duration-75"
    style={{
      left: trail[trail.length - 1].x - 8,
      top: trail[trail.length - 1].y - 8,
    }}
  />
)}
This border-ghost ring (#f4f1ea) provides precise cursor position feedback at the tip of the trail. Its transition-transform duration-75 gives it a very slight lag (75 ms) that visually separates it from the dots beneath.

Full component overview

fixed inset-0           → covers the full viewport
pointer-events-none     → never blocks any mouse events
z-50                    → above all other layers including Navigation
AnimatePresence         → lets Framer Motion run exit animations when dots leave state
trail capped at 15      → max simultaneous dots on screen
500 ms idle clear       → trail dissolves when the mouse stops

Disabling CursorTrail

1

Remove the component from Layout

Open Layout.js and delete the <CursorTrail /> line.
2

Restore the default cursor in CSS

In main.css, remove or comment out the cursor: none rule on body.
body {
  /* cursor: none; */  /* Remove this line */
  background-color: #0b3a44;
  color: #f4f1ea;
}

Customisation reference

PropertyLocationDefault
Trail length.slice(-15)15 dots
Dot colourbg-pumpkin / boxShadow#ff7a1a
Glow spreadboxShadow values10px inner, 20px outer
Fade durationtransition.duration0.5 s
Idle clear delaysetTimeout(..., 500)500 ms
Dot sizew-3 h-312×12 px
Ring colourborder-ghost#f4f1ea

Build docs developers (and LLMs) love