rainbow 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 React, { useEffect, useRef } from 'react';
5
6
interface RainbowCursorProps {
7
element?: HTMLElement;
8
length?: number;
9
colors?: string[];
10
size?: number;
11
trailSpeed?: number;
12
colorCycleSpeed?: number;
13
blur?: number;
14
pulseSpeed?: number;
15
pulseMin?: number;
16
pulseMax?: number;
17
}
18
19
const RainbowCursor: React.FC<RainbowCursorProps> = ({
20
element,
21
length = 20,
22
colors = ['#FE0000', '#FD8C00', '#FFE500', '#119F0B', '#0644B3', '#C22EDC'],
23
size = 3,
24
trailSpeed = 0.4,
25
colorCycleSpeed = 0.002,
26
blur = 0,
27
pulseSpeed = 0.01,
28
pulseMin = 0.8,
29
pulseMax = 1.2,
30
}) => {
31
const canvasRef = useRef<HTMLCanvasElement | null>(null);
32
const contextRef = useRef<CanvasRenderingContext2D | null>(null);
33
const cursorRef = useRef({ x: 0, y: 0 });
34
const particlesRef = useRef<Array<{ position: { x: number; y: number } }>>(
35
[]
36
);
37
const animationFrameRef = useRef<number>();
38
const cursorsInittedRef = useRef(false);
39
const timeRef = useRef(0);
40
41
class Particle {
42
position: { x: number; y: number };
43
44
constructor(x: number, y: number) {
45
this.position = { x, y };
46
}
47
}
48
49
// Helper function to interpolate between colors
50
const interpolateColors = (
51
color1: string,
52
color2: string,
53
factor: number
54
) => {
55
const r1 = parseInt(color1.substr(1, 2), 16);
56
const g1 = parseInt(color1.substr(3, 2), 16);
57
const b1 = parseInt(color1.substr(5, 2), 16);
58
59
const r2 = parseInt(color2.substr(1, 2), 16);
60
const g2 = parseInt(color2.substr(3, 2), 16);
61
const b2 = parseInt(color2.substr(5, 2), 16);
62
63
const r = Math.round(r1 + (r2 - r1) * factor);
64
const g = Math.round(g1 + (g2 - g1) * factor);
65
const b = Math.round(b1 + (b2 - b1) * factor);
66
67
return `rgb(${r}, ${g}, ${b})`;
68
};
69
70
// Function to get dynamic size based on pulse
71
const getPulseSize = (baseSize: number, time: number) => {
72
const pulse = Math.sin(time * pulseSpeed);
73
const scaleFactor = pulseMin + ((pulse + 1) * (pulseMax - pulseMin)) / 2;
74
return baseSize * scaleFactor;
75
};
76
77
useEffect(() => {
78
const hasWrapperEl = element !== undefined;
79
const targetElement = hasWrapperEl ? element : document.body;
80
81
const prefersReducedMotion = window.matchMedia(
82
'(prefers-reduced-motion: reduce)'
83
);
84
85
if (prefersReducedMotion.matches) {
86
console.log('Reduced motion is enabled - cursor animation disabled');
87
return;
88
}
89
90
const canvas = document.createElement('canvas');
91
const context = canvas.getContext('2d', { alpha: true });
92
93
if (!context) return;
94
95
canvasRef.current = canvas;
96
contextRef.current = context;
97
98
canvas.style.top = '0px';
99
canvas.style.left = '0px';
100
canvas.style.pointerEvents = 'none';
101
canvas.style.position = hasWrapperEl ? 'absolute' : 'fixed';
102
103
if (hasWrapperEl) {
104
element?.appendChild(canvas);
105
canvas.width = element.clientWidth;
106
canvas.height = element.clientHeight;
107
} else {
108
document.body.appendChild(canvas);
109
canvas.width = window.innerWidth;
110
canvas.height = window.innerHeight;
111
}
112
113
const onMouseMove = (e: MouseEvent) => {
114
if (hasWrapperEl && element) {
115
const boundingRect = element.getBoundingClientRect();
116
cursorRef.current.x = e.clientX - boundingRect.left;
117
cursorRef.current.y = e.clientY - boundingRect.top;
118
} else {
119
cursorRef.current.x = e.clientX;
120
cursorRef.current.y = e.clientY;
121
}
122
123
if (!cursorsInittedRef.current) {
124
cursorsInittedRef.current = true;
125
for (let i = 0; i < length; i++) {
126
particlesRef.current.push(
127
new Particle(cursorRef.current.x, cursorRef.current.y)
128
);
129
}
130
}
131
};
132
133
const onWindowResize = () => {
134
if (hasWrapperEl && element) {
135
canvas.width = element.clientWidth;
136
canvas.height = element.clientHeight;
137
} else {
138
canvas.width = window.innerWidth;
139
canvas.height = window.innerHeight;
140
}
141
};
142
143
const updateParticles = () => {
144
if (!contextRef.current || !canvasRef.current) return;
145
146
const ctx = contextRef.current;
147
const canvas = canvasRef.current;
148
149
ctx.clearRect(0, 0, canvas.width, canvas.height);
150
ctx.lineJoin = 'round';
151
152
if (blur > 0) {
153
ctx.filter = `blur(${blur}px)`;
154
}
155
156
const particleSets = [];
157
let x = cursorRef.current.x;
158
let y = cursorRef.current.y;
159
160
particlesRef.current.forEach((particle, index) => {
161
const nextParticle =
162
particlesRef.current[index + 1] || particlesRef.current[0];
163
164
particle.position.x = x;
165
particle.position.y = y;
166
167
particleSets.push({ x, y });
168
169
x += (nextParticle.position.x - particle.position.x) * trailSpeed;
170
y += (nextParticle.position.y - particle.position.y) * trailSpeed;
171
});
172
173
// Time-based color cycling
174
timeRef.current += colorCycleSpeed;
175
const colorOffset = timeRef.current % 1;
176
177
// Dynamic size based on pulse
178
const currentSize = getPulseSize(size, timeRef.current);
179
180
colors.forEach((color, index) => {
181
const nextColor = colors[(index + 1) % colors.length];
182
183
ctx.beginPath();
184
ctx.strokeStyle = interpolateColors(
185
color,
186
nextColor,
187
(index + colorOffset) / colors.length
188
);
189
190
if (particleSets.length) {
191
ctx.moveTo(
192
particleSets[0].x,
193
particleSets[0].y + index * (currentSize - 1)
194
);
195
}
196
197
particleSets.forEach((set, particleIndex) => {
198
if (particleIndex !== 0) {
199
ctx.lineTo(set.x, set.y + index * currentSize);
200
}
201
});
202
203
ctx.lineWidth = currentSize;
204
ctx.lineCap = 'round';
205
ctx.stroke();
206
});
207
};
208
209
const loop = () => {
210
updateParticles();
211
animationFrameRef.current = requestAnimationFrame(loop);
212
};
213
214
targetElement.addEventListener('mousemove', onMouseMove);
215
window.addEventListener('resize', onWindowResize);
216
loop();
217
218
return () => {
219
if (canvasRef.current) {
220
canvasRef.current.remove();
221
}
222
if (animationFrameRef.current) {
223
cancelAnimationFrame(animationFrameRef.current);
224
}
225
targetElement.removeEventListener('mousemove', onMouseMove);
226
window.removeEventListener('resize', onWindowResize);
227
};
228
}, [
229
element,
230
length,
231
colors,
232
size,
233
trailSpeed,
234
colorCycleSpeed,
235
blur,
236
pulseSpeed,
237
pulseMin,
238
pulseMax,
239
]);
240
241
return null;
242
};
243
export default RainbowCursor;
244

Props

PropTypeDefaultDescription
elementHTMLElementundefinedThe HTML element where the cursor effect will be applied. Defaults to the entire document.
lengthnumber20The number of particles in the cursor trail.
colorsstring[]['#FE0000', '#FD8C00', '#FFE500', '#119F0B', '#0644B3', '#C22EDC']The array of colors for the cursor trail.
sizenumber3The size of the particles in the cursor trail.
trailSpeednumber0.4The speed at which the trail follows the cursor.
colorCycleSpeednumber0.002The speed of the color transition for the trail.
blurnumber0The amount of blur applied to the trail.
pulseSpeednumber0.01The speed of the pulsing effect for the particle size.
pulseMinnumber0.8The minimum size multiplier for the pulsing effect.
pulseMaxnumber1.2The maximum size multiplier for the pulsing effect.