Skip to main content

Documentation Index

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

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

The Cursor component hides the native browser cursor and replaces it with a rich, three-layer custom cursor: a spring-animated amber dot that tracks the pointer with high-stiffness physics, a trail of fading particles that follow mouse movement, and a rotating sigil that bursts outward on every click. All elements are rendered with pointer-events-none so they never interfere with click or hover targets. The component also exports AnimatePresence as a named re-export used by Layout.

How It Works

The cursor renders inside a single full-viewport wrapper div and is built from three distinct layers:

Dot

A 12 × 12 px filled amber circle (w-3 h-3 bg-amber rounded-full mix-blend-screen) with an amber box-shadow glow. Position is driven by a Framer Motion animate prop with a high-stiffness spring (stiffness: 500, damping: 28, mass: 0.5). When the pointer hovers over a button, link, or any cursor: pointer element, scale animates to 1.5.

Trail

Up to 15 small 6 × 6 px amber particles (w-1.5 h-1.5) emitted every 30 ms during mousemove. Each particle fades out and floats upward over 500 ms via AnimatePresence and exits automatically.

Click burst

On each click event, a 48 × 48 px SVG sigil (crosshair with circle) is spawned at the click coordinates. It rotates from −45° to 45° and scales from 0.5 to 1.5 while fading out over 800 ms, then removes itself.
{/* Dot — spring-animated, scales on hover */}
<motion.div
  className="absolute w-3 h-3 bg-amber rounded-full mix-blend-screen"
  style={{ boxShadow: '0 0 10px 2px rgba(245, 178, 91, 0.8)' }}
  animate={{ x: pos.x - 6, y: pos.y - 6, scale: isHovering ? 1.5 : 1 }}
  transition={{ type: 'spring', stiffness: 500, damping: 28, mass: 0.5 }}
/>

Implementation Details

1

Mouse tracking

Two event listeners are added to window inside a useEffect on component mount: a mousemove handler and a click handler. The mousemove handler updates the pos state { x, y } with clientX/clientY, detects hoverable elements to set isHovering, and throttles trail particle emission to one particle per 30 ms using requestAnimationFrame for cleanup.
const [pos, setPos]           = useState({ x: 0, y: 0 });
const [trail, setTrail]       = useState([]);
const [clicks, setClicks]     = useState([]);
const [isHovering, setIsHovering] = useState(false);
2

Dot positioning and hover scaling

The dot uses Framer Motion’s animate prop with x: pos.x - 6, y: pos.y - 6 to center the 12 px element on the cursor. The scale value toggles between 1 (normal) and 1.5 (hovering) whenever the pointer moves over a button, a, or any element whose computed style returns cursor: pointer. The spring configuration (stiffness: 500, damping: 28, mass: 0.5) keeps the dot responsive while still feeling physically weighted.
3

Trail particles

Each mousemove event that fires more than 30 ms after the previous one pushes a { x, y, id: Date.now() } entry into the trail array (capped at 15 entries). A requestAnimationFrame loop prunes entries older than 500 ms. AnimatePresence wraps the mapped list so each particle plays its exit animation (opacity: 0, scale: 0, y: particle.y - 20) before unmounting.
4

Click burst sigil

Each click event pushes a { x, y, id } entry into the clicks array. A setTimeout of 1000 ms removes it, and AnimatePresence handles the exit animation. The SVG draws a crosshair path (M50 10 L50 90 M10 50 L90 50 M25 25 L75 75 M25 75 L75 25) with an overlaid circle, colored text-violet.

AnimatePresence Re-export

The Cursor module re-exports AnimatePresence as a named export A. Layout imports both Cursor and AnimatePresence from this single file:
// Layout.js import (minified aliases shown for reference)
import { C as Cursor, A as AnimatePresence } from './Cursor.js';
This is why you should not remove or rename Cursor.js without also updating the AnimatePresence import in Layout.js. Both dependencies are coupled in the same module.

Disabling the Custom Cursor

If you need to remove the custom cursor — for accessibility, performance, or theme reasons — you must update both the component tree and the CSS, or users will be left with no cursor at all.
The native cursor is hidden site-wide via body { cursor: none } in main.css, and also explicitly on a, button, input, textarea, and select elements. Removing <Cursor /> from Layout without also restoring those CSS rules will leave the site with no visible cursor. Always update both.
1

Remove Cursor from Layout

Open components/Layout.js and delete the <Cursor /> JSX element and its import. Since AnimatePresence is also exported from Cursor.js, update that import too:
// Remove the Cursor/AnimatePresence import from Cursor.js
import { C as Cursor, A as AnimatePresence } from './Cursor';

// Replace with AnimatePresence from framer-motion directly
import { AnimatePresence } from 'framer-motion';

// Remove this from the JSX
<Cursor />
2

Restore the body cursor in CSS

Open your global stylesheet (main.css or equivalent) and remove or override the cursor: none declarations:
/* Remove or comment out: */
body {
  cursor: none;
}

a,
button,
input,
textarea,
select {
  cursor: none;
}
3

Verify interactive elements

After restoring body { cursor: auto }, check that interactive elements display the correct cursor variant. You may want to explicitly set cursor: pointer on links and buttons to restore the expected browser default:
a,
button {
  cursor: pointer;
}

input,
textarea,
select {
  cursor: text;
}

Accessibility

Hiding the native operating system cursor is a significant departure from standard browser behavior and can create barriers for some users.

Motor impairments

Users who rely on the cursor position as a visual anchor for precise pointing may find a non-standard cursor shape disorienting. The spring-physics animation adds a small but perceptible lag that does not reflect actual pointer position.

User preference

Consider reading a localStorage flag (e.g. vdoom_custom_cursor) to let users opt out. Store the preference in a settings toggle and conditionally render <Cursor /> based on it.
A minimal preference toggle pattern:
// In a settings component or footer
const [customCursor, setCustomCursor] = useState(
  () => localStorage.getItem('vdoom_custom_cursor') !== 'false'
);

const toggle = () => {
  const next = !customCursor;
  setCustomCursor(next);
  localStorage.setItem('vdoom_custom_cursor', String(next));
};

// In Layout — conditionally render
{customCursor && <Cursor />}
When customCursor is false, remember to also toggle the cursor: none CSS back to cursor: auto on the body, otherwise the preference has no visual effect. This can be done by conditionally adding a class to <body> or via a CSS custom property.

Build docs developers (and LLMs) love