Neon Cursor Effect

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

1
// @ts-nocheck
2
'use client';
3
4
import { useState, useEffect, useCallback } from 'react';
5
import { motion, useAnimation } from 'framer-motion';
6
import './NeonCursor.css';
7
// Attach your `neoncursor.css` file in your project.
8
// If you are using React, you can import the CSS directly into your `index.css` or another relevant CSS file.
9
// For Next.js, add the `neoncursor.css` styles to your global CSS file (e.g., `globals.css`).
10
11
const NeonCursor = () => {
12
const [position, setPosition] = useState({
13
x: 0,
14
y: 0,
15
scale: 1,
16
opacity: 1,
17
});
18
const [isClicking, setIsClicking] = useState(false);
19
const [isHovering, setIsHovering] = useState(false);
20
const trailControls = useAnimation();
21
const glowControls = useAnimation();
22
23
const handleMouseMove = useCallback((e) => {
24
setPosition((prev) => ({
25
...prev,
26
x: e.clientX,
27
y: e.clientY,
28
}));
29
}, []);
30
31
const handleMouseDown = () => setIsClicking(true);
32
const handleMouseUp = () => setIsClicking(false);
33
34
const handleMouseOver = useCallback(
35
(e) => {
36
const target = e.target;
37
if (target.matches('a, button, input, [data-hover="true"]')) {
38
setIsHovering(true);
39
void trailControls.start({
40
scale: 1.5,
41
borderColor: 'rgb(255, 150, 50)',
42
borderWidth: '3px',
43
});
44
void glowControls.start({
45
scale: 2,
46
opacity: 0.8,
47
});
48
}
49
},
50
[trailControls, glowControls]
51
);
52
53
const handleMouseOut = useCallback(() => {
54
setIsHovering(false);
55
void trailControls.start({
56
scale: 1,
57
borderColor: 'rgb(236, 101, 23)',
58
borderWidth: '2px',
59
});
60
void glowControls.start({
61
scale: 1,
62
opacity: 0.4,
63
});
64
}, [trailControls, glowControls]);
65
66
useEffect(() => {
67
window.addEventListener('mousemove', handleMouseMove);
68
window.addEventListener('mousedown', handleMouseDown);
69
window.addEventListener('mouseup', handleMouseUp);
70
window.addEventListener('mouseover', handleMouseOver);
71
window.addEventListener('mouseout', handleMouseOut);
72
73
return () => {
74
window.removeEventListener('mousemove', handleMouseMove);
75
window.removeEventListener('mousedown', handleMouseDown);
76
window.removeEventListener('mouseup', handleMouseUp);
77
window.removeEventListener('mouseover', handleMouseOver);
78
window.removeEventListener('mouseout', handleMouseOut);
79
};
80
}, [handleMouseMove, handleMouseOver, handleMouseOut]);
81
82
return (
83
<div className='neon-cursor-container'>
84
{/* Main cursor dot */}
85
<motion.div
86
className='cursor-main'
87
animate={{
88
x: position.x - 10,
89
y: position.y - 10,
90
scale: isClicking ? 0.8 : isHovering ? 1.2 : 1,
91
}}
92
transition={{
93
type: 'spring',
94
damping: 20,
95
stiffness: 400,
96
mass: 0.5,
97
}}
98
/>
99
100
{/* Trailing circle */}
101
<motion.div
102
className='cursor-trail'
103
animate={{
104
x: position.x - 20,
105
y: position.y - 20,
106
}}
107
transition={{
108
type: 'spring',
109
damping: 30,
110
stiffness: 200,
111
mass: 0.8,
112
}}
113
initial={false}
114
/>
115
116
{/* Outer glow */}
117
<motion.div
118
className='cursor-glow'
119
animate={{
120
x: position.x - 30,
121
y: position.y - 30,
122
}}
123
transition={{
124
type: 'spring',
125
damping: 40,
126
stiffness: 150,
127
mass: 1,
128
}}
129
initial={false}
130
/>
131
</div>
132
);
133
};
134
135
export default NeonCursor;
136

useCanvasCursor

1
// @ts-nocheck
2
3
import { useEffect } from 'react';
4
5
const useCanvasCursor = () => {
6
function n(e) {
7
this.init(e || {});
8
}
9
n.prototype = {
10
init: function (e) {
11
this.phase = e.phase || 0;
12
this.offset = e.offset || 0;
13
this.frequency = e.frequency || 0.001;
14
this.amplitude = e.amplitude || 1;
15
},
16
update: function () {
17
return (
18
(this.phase += this.frequency),
19
(e = this.offset + Math.sin(this.phase) * this.amplitude)
20
);
21
},
22
value: function () {
23
return e;
24
},
25
};
26
27
function Line(e) {
28
this.init(e || {});
29
}
30
31
Line.prototype = {
32
init: function (e) {
33
this.spring = e.spring + 0.1 * Math.random() - 0.02;
34
this.friction = E.friction + 0.01 * Math.random() - 0.002;
35
this.nodes = [];
36
for (var t, n = 0; n < E.size; n++) {
37
t = new Node();
38
t.x = pos.x;
39
t.y = pos.y;
40
this.nodes.push(t);
41
}
42
},
43
update: function () {
44
var e = this.spring,
45
t = this.nodes[0];
46
t.vx += (pos.x - t.x) * e;
47
t.vy += (pos.y - t.y) * e;
48
for (var n, i = 0, a = this.nodes.length; i < a; i++)
49
(t = this.nodes[i]),
50
0 < i &&
51
((n = this.nodes[i - 1]),
52
(t.vx += (n.x - t.x) * e),
53
(t.vy += (n.y - t.y) * e),
54
(t.vx += n.vx * E.dampening),
55
(t.vy += n.vy * E.dampening)),
56
(t.vx *= this.friction),
57
(t.vy *= this.friction),
58
(t.x += t.vx),
59
(t.y += t.vy),
60
(e *= E.tension);
61
},
62
draw: function () {
63
var e,
64
t,
65
n = this.nodes[0].x,
66
i = this.nodes[0].y;
67
ctx.beginPath();
68
ctx.moveTo(n, i);
69
for (var a = 1, o = this.nodes.length - 2; a < o; a++) {
70
e = this.nodes[a];
71
t = this.nodes[a + 1];
72
n = 0.5 * (e.x + t.x);
73
i = 0.5 * (e.y + t.y);
74
ctx.quadraticCurveTo(e.x, e.y, n, i);
75
}
76
e = this.nodes[a];
77
t = this.nodes[a + 1];
78
ctx.quadraticCurveTo(e.x, e.y, t.x, t.y);
79
ctx.stroke();
80
ctx.closePath();
81
},
82
};
83
84
function onMousemove(e) {
85
function o() {
86
lines = [];
87
for (var e = 0; e < E.trails; e++)
88
lines.push(new Line({ spring: 0.4 + (e / E.trails) * 0.025 }));
89
}
90
function c(e) {
91
e.touches
92
? ((pos.x = e.touches[0].pageX), (pos.y = e.touches[0].pageY))
93
: ((pos.x = e.clientX), (pos.y = e.clientY)),
94
e.preventDefault();
95
}
96
function l(e) {
97
1 == e.touches.length &&
98
((pos.x = e.touches[0].pageX), (pos.y = e.touches[0].pageY));
99
}
100
document.removeEventListener('mousemove', onMousemove),
101
document.removeEventListener('touchstart', onMousemove),
102
document.addEventListener('mousemove', c),
103
document.addEventListener('touchmove', c),
104
document.addEventListener('touchstart', l),
105
c(e),
106
o(),
107
render();
108
}
109
110
function render() {
111
if (ctx.running) {
112
ctx.globalCompositeOperation = 'source-over';
113
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
114
ctx.globalCompositeOperation = 'lighter';
115
ctx.strokeStyle = 'hsla(' + Math.round(f.update()) + ',50%,50%,0.2)';
116
ctx.lineWidth = 1;
117
for (var e, t = 0; t < E.trails; t++) {
118
(e = lines[t]).update();
119
e.draw();
120
}
121
ctx.frame++;
122
window.requestAnimationFrame(render);
123
}
124
}
125
126
function resizeCanvas() {
127
ctx.canvas.width = window.innerWidth - 20;
128
ctx.canvas.height = window.innerHeight;
129
}
130
131
var ctx,
132
f,
133
e = 0,
134
pos = {},
135
lines = [],
136
E = {
137
debug: true,
138
friction: 0.5,
139
trails: 20,
140
size: 50,
141
dampening: 0.25,
142
tension: 0.98,
143
};
144
function Node() {
145
this.x = 0;
146
this.y = 0;
147
this.vy = 0;
148
this.vx = 0;
149
}
150
151
const renderCanvas = function () {
152
ctx = document.getElementById('canvas').getContext('2d');
153
ctx.running = true;
154
ctx.frame = 1;
155
f = new n({
156
phase: Math.random() * 2 * Math.PI,
157
amplitude: 85,
158
frequency: 0.0015,
159
offset: 285,
160
});
161
document.addEventListener('mousemove', onMousemove);
162
document.addEventListener('touchstart', onMousemove);
163
document.body.addEventListener('orientationchange', resizeCanvas);
164
window.addEventListener('resize', resizeCanvas);
165
window.addEventListener('focus', () => {
166
if (!ctx.running) {
167
ctx.running = true;
168
render();
169
}
170
});
171
window.addEventListener('blur', () => {
172
ctx.running = true;
173
});
174
resizeCanvas();
175
};
176
177
useEffect(() => {
178
renderCanvas();
179
180
return () => {
181
ctx.running = false;
182
document.removeEventListener('mousemove', onMousemove);
183
document.removeEventListener('touchstart', onMousemove);
184
document.body.removeEventListener('orientationchange', resizeCanvas);
185
window.removeEventListener('resize', resizeCanvas);
186
window.removeEventListener('focus', () => {
187
if (!ctx.running) {
188
ctx.running = true;
189
render();
190
}
191
});
192
window.removeEventListener('blur', () => {
193
ctx.running = true;
194
});
195
};
196
}, []);
197
};
198
199
export default useCanvasCursor;

Props

PropTypeDefaultDescription
classnamestringOptional CSS class for styling the main vignette container.
childrenReact.ReactNodeThe content to display inside the vignette effect.
radiusstring24pxThe radius for the vignette effect.
insetstring20pxThe inset value for the vignette effect.
transitionLengthstring44pxThe length of the transition effect applied to the vignette.
blurstring6pxThe blur amount for the vignette effect.
blurclassnamestringOptional CSS class for styling the blur effect container.