Fairy Dust Cursor Effect

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

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, 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({ width: 0, height: 0 });
47
48
useEffect(() => {
49
const canvas = canvasRef.current;
50
if (!canvas) return;
51
52
const targetElement = element || document.body;
53
const context = canvas.getContext('2d');
54
if (!context) return;
55
56
const updateCanvasSize = () => {
57
const newWidth = element ? targetElement.clientWidth : window.innerWidth;
58
const newHeight = element
59
? targetElement.clientHeight
60
: window.innerHeight;
61
setCanvasSize({ width: newWidth, height: newHeight });
62
};
63
64
updateCanvasSize();
65
window.addEventListener('resize', updateCanvasSize);
66
67
// Animation frame setup
68
let animationFrameId: number;
69
70
const createParticle = (x: number, y: number): Particle => {
71
const randomChar =
72
characterSet[Math.floor(Math.random() * characterSet.length)];
73
const randomColor = colors[Math.floor(Math.random() * colors.length)];
74
const velocityX =
75
(Math.random() < 0.5 ? -1 : 1) *
76
(Math.random() * (initialVelocity.max - initialVelocity.min) +
77
initialVelocity.min);
78
const velocityY = -(Math.random() * initialVelocity.max);
79
80
return {
81
x,
82
y,
83
character: randomChar,
84
color: randomColor,
85
velocity: { x: velocityX, y: velocityY },
86
lifeSpan: 100,
87
initialLifeSpan: 100,
88
scale: 1,
89
};
90
};
91
92
const updateParticles = () => {
93
if (!context) return;
94
context.clearRect(0, 0, canvasSize.width, canvasSize.height);
95
96
// Update and draw particles
97
particlesRef.current.forEach((particle, index) => {
98
// Update position
99
particle.x += particle.velocity.x;
100
particle.y += particle.velocity.y;
101
102
// Apply gravity
103
particle.velocity.y += gravity;
104
105
// Update lifespan and scale
106
particle.lifeSpan *= fadeSpeed;
107
particle.scale = Math.max(
108
particle.lifeSpan / particle.initialLifeSpan,
109
0
110
);
111
112
// Draw particle
113
context.save();
114
context.font = `${particleSize * particle.scale}px serif`;
115
context.fillStyle = particle.color;
116
context.globalAlpha = particle.scale;
117
context.fillText(particle.character, particle.x, particle.y);
118
context.restore();
119
});
120
121
// Remove dead particles
122
particlesRef.current = particlesRef.current.filter(
123
(particle) => particle.lifeSpan > 0.1
124
);
125
};
126
127
const animate = () => {
128
updateParticles();
129
animationFrameId = requestAnimationFrame(animate);
130
};
131
132
const handleMouseMove = (e: MouseEvent) => {
133
const rect = element ? targetElement.getBoundingClientRect() : undefined;
134
const x = element ? e.clientX - rect!.left : e.clientX;
135
const y = element ? e.clientY - rect!.top : e.clientY;
136
137
cursorRef.current = { x, y };
138
139
const distance = Math.hypot(
140
cursorRef.current.x - lastPosRef.current.x,
141
cursorRef.current.y - lastPosRef.current.y
142
);
143
144
if (distance > 2) {
145
for (let i = 0; i < particleCount; i++) {
146
particlesRef.current.push(
147
createParticle(cursorRef.current.x, cursorRef.current.y)
148
);
149
}
150
lastPosRef.current = { ...cursorRef.current };
151
}
152
};
153
154
const handleTouchMove = (e: TouchEvent) => {
155
e.preventDefault();
156
const touch = e.touches[0];
157
const rect = element ? targetElement.getBoundingClientRect() : undefined;
158
const x = element ? touch.clientX - rect!.left : touch.clientX;
159
const y = element ? touch.clientY - rect!.top : touch.clientY;
160
161
for (let i = 0; i < particleCount; i++) {
162
particlesRef.current.push(createParticle(x, y));
163
}
164
};
165
166
targetElement.addEventListener('mousemove', handleMouseMove);
167
targetElement.addEventListener('touchmove', handleTouchMove, {
168
passive: false,
169
});
170
animate();
171
172
return () => {
173
window.removeEventListener('resize', updateCanvasSize);
174
targetElement.removeEventListener('mousemove', handleMouseMove);
175
targetElement.removeEventListener('touchmove', handleTouchMove);
176
cancelAnimationFrame(animationFrameId);
177
};
178
}, [
179
colors,
180
element,
181
characterSet,
182
particleSize,
183
particleCount,
184
gravity,
185
fadeSpeed,
186
initialVelocity,
187
]);
188
189
return (
190
<canvas
191
ref={canvasRef}
192
width={canvasSize.width}
193
height={canvasSize.height}
194
style={{
195
position: element ? 'absolute' : 'fixed',
196
top: 0,
197
left: 0,
198
pointerEvents: 'none',
199
zIndex: 9999,
200
}}
201
/>
202
);
203
};
204
205
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.