Fairy Dust Cursor Effect
An interactive React component that creates magical fairy dust particle trails following cursor with sparkling emoji effects.
An interactive React component that creates magical fairy dust particle trails following cursor with sparkling emoji effects.
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. |