Fairy Dust Cursor Effect

An interactive React component that creates magical fairy dust particle trails following cursor with sparkling emoji effects.

1
'use client';
2
import React from 'react';
3
import FairyDustCursor from './FairyDustCursor';
4
5
function FairyDustIndex() {
6
return (
7
<>
8
<FairyDustCursor
9
colors={['#FF0000', '#00FF00', '#0000FF']}
10
characterSet={['✨', '⭐', '🌟']}
11
particleSize={24}
12
particleCount={2}
13
gravity={0.015}
14
fadeSpeed={0.97}
15
initialVelocity={{ min: 0.7, max: 2.0 }}
16
/>
17
</>
18
);
19
}
20
21
export default FairyDustIndex;
22

fairydust

1
'use client';
2
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
3
4
interface FairyDustCursorProps {
5
colors?: string[];
6
element?: HTMLElement;
7
characterSet?: string[];
8
particleSize?: number;
9
particleCount?: number;
10
gravity?: number;
11
fadeSpeed?: number;
12
initialVelocity?: {
13
min: number;
14
max: number;
15
};
16
}
17
18
interface Particle {
19
x: number;
20
y: number;
21
character: string;
22
color: string;
23
velocity: {
24
x: number;
25
y: number;
26
};
27
lifeSpan: number;
28
initialLifeSpan: number;
29
scale: number;
30
}
31
32
export const FairyDustCursor: React.FC<FairyDustCursorProps> = ({
33
colors = ['#D61C59', '#E7D84B', '#1B8798'],
34
element,
35
characterSet = ['✨', '⭐', '🌟', '★', '*'],
36
particleSize = 21,
37
particleCount = 1,
38
gravity = 0.02,
39
fadeSpeed = 0.98,
40
initialVelocity = { min: 0.5, max: 1.5 },
41
}) => {
42
const canvasRef = useRef<HTMLCanvasElement>(null);
43
const particlesRef = useRef<Particle[]>([]);
44
const cursorRef = useRef({ x: 0, y: 0 });
45
const lastPosRef = useRef({ x: 0, y: 0 });
46
const [canvasSize, setCanvasSize] = useState({
47
width: element ? element.clientWidth : window.innerWidth,
48
height: element ? element.clientHeight : window.innerHeight,
49
});
50
51
useLayoutEffect(() => {
52
const updateCanvasSize = () => {
53
const newWidth = element ? element.clientWidth : window.innerWidth;
54
const newHeight = element ? element.clientHeight : window.innerHeight;
55
56
console.log('vavva updateCanvasSize', newWidth, newHeight);
57
setCanvasSize({ width: newWidth, height: newHeight });
58
};
59
60
updateCanvasSize();
61
window.addEventListener('resize', updateCanvasSize);
62
63
return () => {
64
window.removeEventListener('resize', updateCanvasSize);
65
};
66
}, [element]);
67
68
useEffect(() => {
69
const canvas = canvasRef.current;
70
if (!canvas) return;
71
72
const targetElement = element || document.body;
73
const context = canvas.getContext('2d');
74
if (!context) return;
75
76
canvas.width = canvasSize.width;
77
canvas.height = canvasSize.height;
78
79
// Animation frame setup
80
let animationFrameId: number;
81
82
const createParticle = (x: number, y: number): Particle => {
83
const randomChar =
84
characterSet[Math.floor(Math.random() * characterSet.length)];
85
const randomColor = colors[Math.floor(Math.random() * colors.length)];
86
const velocityX =
87
(Math.random() < 0.5 ? -1 : 1) *
88
(Math.random() * (initialVelocity.max - initialVelocity.min) +
89
initialVelocity.min);
90
const velocityY = -(Math.random() * initialVelocity.max);
91
92
return {
93
x,
94
y,
95
character: randomChar,
96
color: randomColor,
97
velocity: { x: velocityX, y: velocityY },
98
lifeSpan: 100,
99
initialLifeSpan: 100,
100
scale: 1,
101
};
102
};
103
104
const updateParticles = () => {
105
if (!context) return;
106
context.clearRect(0, 0, canvasSize.width, canvasSize.height);
107
108
// Update and draw particles
109
particlesRef.current.forEach((particle, index) => {
110
// Update position
111
particle.x += particle.velocity.x;
112
particle.y += particle.velocity.y;
113
114
// Apply gravity
115
particle.velocity.y += gravity;
116
117
// Update lifespan and scale
118
particle.lifeSpan *= fadeSpeed;
119
particle.scale = Math.max(
120
particle.lifeSpan / particle.initialLifeSpan,
121
0
122
);
123
124
// Draw particle
125
context.save();
126
context.font = `${particleSize * particle.scale}px serif`;
127
context.fillStyle = particle.color;
128
context.globalAlpha = particle.scale;
129
context.fillText(particle.character, particle.x, particle.y);
130
context.restore();
131
});
132
133
// Remove dead particles
134
particlesRef.current = particlesRef.current.filter(
135
(particle) => particle.lifeSpan > 0.1
136
);
137
};
138
139
const animate = () => {
140
updateParticles();
141
animationFrameId = requestAnimationFrame(animate);
142
};
143
144
const handleMouseMove = (e: MouseEvent) => {
145
const rect = element ? targetElement.getBoundingClientRect() : undefined;
146
const x = element ? e.clientX - rect!.left : e.clientX;
147
const y = element ? e.clientY - rect!.top : e.clientY;
148
149
cursorRef.current = { x, y };
150
151
const distance = Math.hypot(
152
cursorRef.current.x - lastPosRef.current.x,
153
cursorRef.current.y - lastPosRef.current.y
154
);
155
156
if (distance > 2) {
157
for (let i = 0; i < particleCount; i++) {
158
particlesRef.current.push(
159
createParticle(cursorRef.current.x, cursorRef.current.y)
160
);
161
}
162
lastPosRef.current = { ...cursorRef.current };
163
}
164
};
165
166
const handleTouchMove = (e: TouchEvent) => {
167
e.preventDefault();
168
const touch = e.touches[0];
169
const rect = element ? targetElement.getBoundingClientRect() : undefined;
170
const x = element ? touch.clientX - rect!.left : touch.clientX;
171
const y = element ? touch.clientY - rect!.top : touch.clientY;
172
173
for (let i = 0; i < particleCount; i++) {
174
particlesRef.current.push(createParticle(x, y));
175
}
176
};
177
178
targetElement.addEventListener('mousemove', handleMouseMove);
179
targetElement.addEventListener('touchmove', handleTouchMove, {
180
passive: false,
181
});
182
animate();
183
184
return () => {
185
targetElement.removeEventListener('mousemove', handleMouseMove);
186
targetElement.removeEventListener('touchmove', handleTouchMove);
187
cancelAnimationFrame(animationFrameId);
188
};
189
}, [
190
colors,
191
element,
192
characterSet,
193
particleSize,
194
particleCount,
195
gravity,
196
fadeSpeed,
197
initialVelocity,
198
canvasSize,
199
]);
200
201
return (
202
<canvas
203
ref={canvasRef}
204
width={canvasSize.width}
205
height={canvasSize.height}
206
style={{
207
position: element ? 'absolute' : 'fixed',
208
top: 0,
209
left: 0,
210
pointerEvents: 'none',
211
zIndex: 9999,
212
}}
213
/>
214
);
215
};
216
217
export default FairyDustCursor;

Props

PropTypeDefaultDescription
colorsstring[]['#D61C59', '#E7D84B', '#1B8798']Array of colors for the particles.
elementHTMLElementundefinedThe HTML element where the cursor effect will be applied. If not specified, the effect applies to the document.
characterSetstring[]['✨', '⭐', '🌟', '★', '*']Array of characters used for particles.
particleSizenumber21Size of the particles in pixels.
particleCountnumber1Number of particles generated per cursor movement event.
gravitynumber0.02Gravity effect applied to the particles.
fadeSpeednumber0.98The fade-out speed of the particles (value between 0 and 1).
initialVelocity{ min: number, max: number }{ min: 0.5, max: 1.5 }The initial velocity range for particles.