Rainbow Cursor Effect

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

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.