Neon Cursor Effect

An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.

1
'use client';
2
3
import React, { useReducer, useEffect } from 'react';
4
5
// Define the shape of a ripple object
6
interface Ripple {
7
id: string;
8
x: number;
9
y: number;
10
}
11
12
// Props for the RippleCursor component
13
interface RippleCursorProps {
14
maxSize?: number; // Maximum size of the ripple
15
duration?: number; // Duration of the ripple animation in milliseconds
16
blur?: boolean; // Whether the ripple has a blur effect
17
}
18
19
// Type for the reducer's state
20
type RippleState = Ripple[];
21
22
// Type for the reducer's actions
23
type RippleAction =
24
| { type: 'ADD_RIPPLE'; payload: Ripple }
25
| { type: 'REMOVE_RIPPLE'; payload: string };
26
27
// Reducer function
28
const rippleReducer = (
29
state: RippleState,
30
action: RippleAction
31
): RippleState => {
32
switch (action.type) {
33
case 'ADD_RIPPLE':
34
return [...state, action.payload].slice(-30); // Limit ripple count
35
case 'REMOVE_RIPPLE':
36
return state.filter((ripple) => ripple.id !== action.payload);
37
default:
38
return state;
39
}
40
};
41
42
// Component definition
43
const RippleCursor: React.FC<RippleCursorProps> = ({
44
maxSize = 50,
45
duration = 1000,
46
blur = true,
47
}) => {
48
const [ripples, dispatch] = useReducer(rippleReducer, []);
49
50
// Event handler for mouse movements
51
const handleMouseMove = (e: MouseEvent): void => {
52
const ripple: Ripple = {
53
id: `${Date.now()}-${Math.random()}`,
54
x: e.clientX,
55
y: e.clientY,
56
};
57
58
dispatch({ type: 'ADD_RIPPLE', payload: ripple });
59
60
// Remove ripple after the animation duration
61
setTimeout(() => {
62
dispatch({ type: 'REMOVE_RIPPLE', payload: ripple.id });
63
}, duration);
64
};
65
66
// Effect to attach and detach the mousemove event listener
67
useEffect(() => {
68
window.addEventListener('mousemove', handleMouseMove);
69
70
return () => {
71
window.removeEventListener('mousemove', handleMouseMove);
72
};
73
}, [duration]);
74
75
return (
76
<div className='fixed top-0 left-0 w-screen h-screen pointer-events-none overflow-hidden z-[9999]'>
77
{ripples.map((ripple) => (
78
<div
79
key={ripple.id}
80
className='absolute rounded-full bg-blue-500 bg-opacity-50 shadow-[0_0_10px_rgba(0,150,255,0.7),0_0_20px_rgba(0,150,255,0.4)] animate-ripple'
81
style={{
82
left: `${ripple.x}px`,
83
top: `${ripple.y}px`,
84
width: `${maxSize}px`,
85
height: `${maxSize}px`,
86
animationDuration: `${duration}ms`,
87
filter: blur ? 'blur(4px)' : 'none',
88
}}
89
/>
90
))}
91
</div>
92
);
93
};
94
95
export default RippleCursor;
96