Skip to main content

Documentation Index

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

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

CustomCursor overrides the browser’s default pointer with a custom pixel-art SVG cursor and a short magenta particle trail. It uses fixed positioning and a high z-index to float above all other content, and it adds the custom-cursor-active class to document.body to hide the system cursor on all child elements.

No props

CustomCursor accepts no props. Mount it once at the application root, as a sibling to the main app shell:
// In your root App component
function App() {
  return (
    <>
      <CustomCursor />
      <FakeBrowserChrome>
        {/* rest of the app */}
      </FakeBrowserChrome>
    </>
  );
}
Placing CustomCursor outside FakeBrowserChrome ensures the custom cursor covers the entire viewport, including the browser-chrome title bar and toolbar, not just the content area.

How it works

Mouse tracking

Inside a useEffect that runs once on mount, CustomCursor attaches a mousemove listener to window:
useEffect(() => {
  let trailId = 0;

  const handleMouseMove = (e) => {
    // Update primary cursor position
    setPosition({ x: e.clientX, y: e.clientY });

    // Append a new trail particle (max 5 kept at any time)
    setTrail((prev) => {
      const next = [...prev, { x: e.clientX, y: e.clientY, id: trailId++ }];
      if (next.length > 5) next.shift();
      return next;
    });
  };

  window.addEventListener("mousemove", handleMouseMove);
  document.body.classList.add("custom-cursor-active");

  return () => {
    window.removeEventListener("mousemove", handleMouseMove);
    document.body.classList.remove("custom-cursor-active");
  };
}, []);
The cleanup function removes both the event listener and the body class when the component unmounts — for example, if you conditionally render CustomCursor only on desktop breakpoints.

Trail particle decay

A second useEffect watches the trail state array. Whenever new particles are present, it schedules a 50 ms setTimeout that removes the oldest particle from the front of the array:
useEffect(() => {
  if (trail.length > 0) {
    const timer = setTimeout(() => {
      setTrail((prev) => prev.slice(1));
    }, 50);
    return () => clearTimeout(timer);
  }
}, [trail]);
This creates a smooth fade-out effect: particles briefly linger behind the cursor then disappear in the order they were added.

The custom-cursor-active body class

Adding custom-cursor-active to document.body is the trigger for a CSS rule in assets/main.css that hides the default cursor across all elements:
.custom-cursor-active,
.custom-cursor-active * {
  cursor: none !important;
}
Without this rule the custom SVG would render on top of the native cursor rather than replacing it.

Rendered output

CustomCursor renders a React Fragment with two layers:

1. Primary cursor SVG

<div
  className="fixed top-0 left-0 pointer-events-none z-[9999] mix-blend-difference"
  style={{ transform: `translate(${position.x}px, ${position.y}px)` }}
>
  <svg
    width="24"
    height="24"
    viewBox="0 0 24 24"
    className="pixel-art fill-y2k-cyan drop-shadow-[2px_2px_0_rgba(255,0,229,0.5)]"
  >
    <path d="M0 0 L16 6 L9 9 L13 16 L10 18 L6 11 L0 16 Z" />
  </svg>
</div>
Key details:
  • Anchored at top-0 left-0 and moved with CSS transform: translate(x, y) for GPU-accelerated, jank-free tracking.
  • mix-blend-difference inverts the colours of whatever is beneath the cursor, making it visible on both light and dark backgrounds.
  • pointer-events-none ensures the cursor overlay never intercepts clicks intended for elements below it.
  • z-[9999] keeps it above all other content including FakeBrowserChrome.

2. Trail particles

Each of up to five trail particles renders as a small w-2 h-2 (8×8 px) magenta square at z-[9998]:
{trail.map((dot, index) => (
  <div
    key={dot.id}
    className="fixed top-0 left-0 pointer-events-none z-[9998] w-2 h-2 bg-y2k-magenta"
    style={{
      transform: `translate(${dot.x + 4}px, ${dot.y + 4}px)`,
      opacity: ((index + 1) / trail.length) * 0.5,
    }}
  />
))}
The opacity is graduated: older particles (lower index) are more transparent and newer particles are more opaque, up to a maximum of 0.5. The +4 pixel offset centres each particle beneath the cursor tip.

Fallback behaviour

CustomCursor only activates when JavaScript is running. If JavaScript is disabled or blocked, the custom-cursor-active class is never added to document.body and the browser’s default system cursor is used instead — no functionality is lost.
On touch devices (phones and tablets) where there is no mouse pointer, the mousemove event never fires. CustomCursor mounts without error but renders nothing visible. The custom-cursor-active class is still added to document.body, but since there is no pointer, it has no practical effect.
If you want to restrict the custom cursor to desktop viewports only, wrap the CustomCursor render in a media-query check using a useMediaQuery hook, or conditionally render it based on a window.matchMedia("(pointer: fine)") test inside the useEffect.

Build docs developers (and LLMs) love