Fairy Dust Cursor Effect
An interactive React component that creates a magical fairy dust effect, generating a trail of sparkling emoji particles that follow the cursor in real time. ✨
An interactive React component that creates a magical fairy dust effect, generating a trail of sparkling emoji particles that follow the cursor in real time. ✨
1'use client';2import React from 'react';3import FairyDustCursor from './FairyDustCursor';45function FairyDustIndex() {6return (7<>8<FairyDustCursor9colors={['#FF0000', '#00FF00', '#0000FF']}10characterSet={['✨', '⭐', '🌟']}11particleSize={24}12particleCount={2}13gravity={0.015}14fadeSpeed={0.97}15initialVelocity={{ min: 0.7, max: 2.0 }}16/>17</>18);19}2021export default FairyDustIndex;22
1'use client';2import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';34interface FairyDustCursorProps {5colors?: string[];6element?: HTMLElement;7characterSet?: string[];8particleSize?: number;9particleCount?: number;10gravity?: number;11fadeSpeed?: number;12initialVelocity?: {13min: number;14max: number;15};16}1718interface Particle {19x: number;20y: number;21character: string;22color: string;23velocity: {24x: number;25y: number;26};27lifeSpan: number;28initialLifeSpan: number;29scale: number;30}3132export const FairyDustCursor: React.FC<FairyDustCursorProps> = ({33colors = ['#D61C59', '#E7D84B', '#1B8798'],34element,35characterSet = ['✨', '⭐', '🌟', '★', '*'],36particleSize = 21,37particleCount = 1,38gravity = 0.02,39fadeSpeed = 0.98,40initialVelocity = { min: 0.5, max: 1.5 },41}) => {42const canvasRef = useRef<HTMLCanvasElement>(null);43const particlesRef = useRef<Particle[]>([]);44const cursorRef = useRef({ x: 0, y: 0 });45const lastPosRef = useRef({ x: 0, y: 0 });46const [canvasSize, setCanvasSize] = useState({47width: element ? element.clientWidth : window.innerWidth,48height: element ? element.clientHeight : window.innerHeight,49});5051useLayoutEffect(() => {52const updateCanvasSize = () => {53const newWidth = element ? element.clientWidth : window.innerWidth;54const newHeight = element ? element.clientHeight : window.innerHeight;5556console.log('vavva updateCanvasSize', newWidth, newHeight);57setCanvasSize({ width: newWidth, height: newHeight });58};5960updateCanvasSize();61window.addEventListener('resize', updateCanvasSize);6263return () => {64window.removeEventListener('resize', updateCanvasSize);65};66}, [element]);6768useEffect(() => {69const canvas = canvasRef.current;70if (!canvas) return;7172const targetElement = element || document.body;73const context = canvas.getContext('2d');74if (!context) return;7576canvas.width = canvasSize.width;77canvas.height = canvasSize.height;7879// Animation frame setup80let animationFrameId: number;8182const createParticle = (x: number, y: number): Particle => {83const randomChar =84characterSet[Math.floor(Math.random() * characterSet.length)];85const randomColor = colors[Math.floor(Math.random() * colors.length)];86const velocityX =87(Math.random() < 0.5 ? -1 : 1) *88(Math.random() * (initialVelocity.max - initialVelocity.min) +89initialVelocity.min);90const velocityY = -(Math.random() * initialVelocity.max);9192return {93x,94y,95character: randomChar,96color: randomColor,97velocity: { x: velocityX, y: velocityY },98lifeSpan: 100,99initialLifeSpan: 100,100scale: 1,101};102};103104const updateParticles = () => {105if (!context) return;106context.clearRect(0, 0, canvasSize.width, canvasSize.height);107108// Update and draw particles109particlesRef.current.forEach((particle, index) => {110// Update position111particle.x += particle.velocity.x;112particle.y += particle.velocity.y;113114// Apply gravity115particle.velocity.y += gravity;116117// Update lifespan and scale118particle.lifeSpan *= fadeSpeed;119particle.scale = Math.max(120particle.lifeSpan / particle.initialLifeSpan,1210122);123124// Draw particle125context.save();126context.font = `${particleSize * particle.scale}px serif`;127context.fillStyle = particle.color;128context.globalAlpha = particle.scale;129context.fillText(particle.character, particle.x, particle.y);130context.restore();131});132133// Remove dead particles134particlesRef.current = particlesRef.current.filter(135(particle) => particle.lifeSpan > 0.1136);137};138139const animate = () => {140updateParticles();141animationFrameId = requestAnimationFrame(animate);142};143144const handleMouseMove = (e: MouseEvent) => {145const rect = element ? targetElement.getBoundingClientRect() : undefined;146const x = element ? e.clientX - rect!.left : e.clientX;147const y = element ? e.clientY - rect!.top : e.clientY;148149cursorRef.current = { x, y };150151const distance = Math.hypot(152cursorRef.current.x - lastPosRef.current.x,153cursorRef.current.y - lastPosRef.current.y154);155156if (distance > 2) {157for (let i = 0; i < particleCount; i++) {158particlesRef.current.push(159createParticle(cursorRef.current.x, cursorRef.current.y)160);161}162lastPosRef.current = { ...cursorRef.current };163}164};165166const handleTouchMove = (e: TouchEvent) => {167e.preventDefault();168const touch = e.touches[0];169const rect = element ? targetElement.getBoundingClientRect() : undefined;170const x = element ? touch.clientX - rect!.left : touch.clientX;171const y = element ? touch.clientY - rect!.top : touch.clientY;172173for (let i = 0; i < particleCount; i++) {174particlesRef.current.push(createParticle(x, y));175}176};177178targetElement.addEventListener('mousemove', handleMouseMove);179targetElement.addEventListener('touchmove', handleTouchMove, {180passive: false,181});182animate();183184return () => {185targetElement.removeEventListener('mousemove', handleMouseMove);186targetElement.removeEventListener('touchmove', handleTouchMove);187cancelAnimationFrame(animationFrameId);188};189}, [190colors,191element,192characterSet,193particleSize,194particleCount,195gravity,196fadeSpeed,197initialVelocity,198canvasSize,199]);200201return (202<canvas203ref={canvasRef}204width={canvasSize.width}205height={canvasSize.height}206style={{207position: element ? 'absolute' : 'fixed',208top: 0,209left: 0,210pointerEvents: 'none',211zIndex: 9999,212}}213/>214);215};216217export default FairyDustCursor;
| Prop | Type | Default | Description |
|---|---|---|---|
colors | string[] | ['#D61C59', '#E7D84B', '#1B8798'] | Array of colors for the particles. |
element | HTMLElement | undefined | The HTML element where the cursor effect will be applied. If not specified, the effect applies to the document. |
characterSet | string[] | ['✨', '⭐', '🌟', '★', '*'] | Array of characters used for particles. |
particleSize | number | 21 | Size of the particles in pixels. |
particleCount | number | 1 | Number of particles generated per cursor movement event. |
gravity | number | 0.02 | Gravity effect applied to the particles. |
fadeSpeed | number | 0.98 | The 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. |