Documentation Index
Fetch the complete documentation index at: https://mintlify.com/apursley2012/witch-dev/llms.txt
Use this file to discover all available pages before exploring further.
CursorTrail replaces the default browser cursor with a custom two-part effect: a glowing acid-green circle that follows the pointer with a slight easing lag, and a fading trail of up to 15 purple dots that evaporate behind it. The entire component is non-interactive (pointer-events: none) and renders at z-[100], placing it above every other element in the stacking context including the mobile menu overlay.
Cursor element
The primary cursor is a 16 × 16px circle rendered as a Framer Motionmotion.div:
bg-coven-green-400— solid acid-green fill that pops against the near-black portfolio background.mix-blend-screen— the cursor brightens whatever is beneath it rather than covering it, so text and UI elements remain readable through the glow.blur-[2px]— softens the hard circle edge into a diffuse light source.boxShadow: '0 0 15px 5px rgba(163, 230, 53, 0.6)'— extends the glow effect well beyond the element’s own 16px boundary, creating a halo radius of roughly 20px on each side.
Easing
type: 'tween', ease: 'backOut', duration: 0.1 gives the cursor a slight overshoot (“backOut”) as it settles on the target position. This tiny elastic snap adds a magical, alive quality to cursor movement without feeling sluggish.
Trail mechanism
Mouse position is tracked with amousemove event listener mounted in a useEffect. On each event, two pieces of state update:
cursor— the current{x, y}position used by the main cursor element.trail— a rolling array of the most recent positions, capped at 15 entries.
id counter (closed over in the useEffect) provides a stable React key for each trail dot without relying on array index, which prevents Framer Motion from reusing animation state across positions.
Prune interval
A separatesetInterval ticks every 50ms to remove the oldest dot from the trail array. This ensures trail dots disappear even when the mouse is stationary — they don’t linger indefinitely once the pointer stops moving.
() => clearInterval(t) is critical — without it, the interval would continue running after the component unmounts, updating state on an unmounted component.
Trail dot styling
Each dot in thetrail array renders as an 8 × 8px purple circle:
initial— each dot spawns at full opacity and full scale at the exact mouse position captured at birth (dot.x,dot.y). It does not follow the cursor after spawning.animate— Framer Motion immediately begins transitioning the dot toopacity: 0, scale: 0over 500ms. By the time a dot reaches 0 it is effectively invisible and zero-sized.bg-coven-purple-400with a magenta-purpleboxShadow— the purple color contrast against the green cursor makes the trail read as a comet tail, reinforcing the directional movement cue.mix-blend-screen— matches the cursor blend mode so both elements interact with background content consistently.
Z-index
The component’s root element sits atz-[100]:
z-[100] the cursor trail renders above:
| Element | Z-index |
|---|---|
| Background effects | z-0 |
| Page content | z-10 (typical) |
| Desktop navigation sidebar | z-50 |
| Mobile menu overlay | z-[60] |
| Cursor trail | z-[100] |
pointer-events: none
The entire component is wrapped in pointer-events-none. Without this, the fixed inset-0 container would block all mouse events from reaching page content — links, buttons, and interactive elements would become unclickable. The custom cursor is purely cosmetic.
Performance tip
The combination of a max-15 trail cap and the 50ms prune interval keeps the live DOM node count bounded at a maximum of 16motion.div elements (1 cursor + 15 trail dots) at any moment. This matters because each motion.div maintains its own Framer Motion animation subscription.
Framer Motion drives the opacity and scale exit animations via the WAAPI (Web Animations API) where supported, offloading work to the compositor thread and keeping the main thread available for React rendering. The useMemo-free trail is intentional: trail positions must be fresh on every mouse event, so memoization would provide no benefit here.