Neon Glow Cursor Design
Summary
Add a custom neon glow cursor with a soft fading trail to the site. The cursor replaces the default browser cursor everywhere, shifting color on interactive elements, and is disabled on touch devices.
Approach
Pure CSS cursor dot + Canvas trail (no dependencies).
- A small fixed-position
<div>acts as the visible cursor dot with a neon glow viabox-shadow - A full-viewport
<canvas>overlay renders the fading trail using per-frame alpha decay - ~50-80 lines of vanilla JS in an Astro component’s
<script>tag
Design
Cursor Dot
- 8px diameter circle,
position: fixed,pointer-events: none background: var(--color-neon)(#39ff14)- Multi-layer
box-shadowfor glow: inner bright core + outer soft spread - Positioned via
mousemovelistener usingrequestAnimationFrame z-index: 9999to float above all content
Trail Canvas
- Full-viewport
<canvas>,position: fixed,pointer-events: none,z-index: 9998(below the dot) - Each frame: draw a small glowing circle at cursor position with radial gradient
- Apply a semi-transparent fill over the whole canvas each frame to fade older points
- Trail length effectively ~20 frames before fully faded
- Canvas context created with
willReadFrequently: truehint
Interactive Element Behavior
When hovering clickable elements (a, button, input, select, textarea, [role="button"], or anything with cursor-pointer):
- Dot shifts to teal (
var(--color-teal), #00e5a0) - Dot scales up to 12px diameter
- Transition is smooth via CSS
transitionon the dot element
Detection: JS mouseover/mouseout event delegation on document, checking element.closest() against the interactive selectors.
Touch Device Handling
- Detect via
matchMedia('(pointer: coarse)')on load - If touch device: do not render the cursor dot or canvas, do not set
cursor: none - This preserves the native mobile experience entirely
Page Transition Support
- Listen for Astro’s
astro:page-loadevent to re-initialize after client-side navigation - Clean up previous listeners/animation frames on
astro:before-preparation
CSS Changes
- Add
cursor: nonetohtmlinBaseLayout.css(scoped to non-touch via JS class toggle) - The JS adds a
.has-custom-cursorclass to<html>and the CSS rule targets that class
File Structure
app/src/components/NeonCursor/NeonCursor.astro— new component (HTML + client-side<script>)app/src/layouts/BaseLayout/BaseLayout.css— addhtml.has-custom-cursor { cursor: none; }andhtml.has-custom-cursor * { cursor: none !important; }app/src/layouts/BaseLayout/BaseLayout.astro— import and render<NeonCursor />
Performance
- Single
requestAnimationFrameloop for both dot positioning and canvas trail - Canvas fade uses
fillRectwith low alpha rather than per-point tracking - No dependencies, no DOM mutations per frame (except dot transform)
- Disabled entirely on touch devices