rainbow Cursor Effect
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
An interactive React component that adds a dynamic bubble effect, visually tracking cursor movement in real time.
1// @ts-nocheck2'use client';34import React, { useEffect, useRef } from 'react';56interface RainbowCursorProps {7element?: HTMLElement;8length?: number;9colors?: string[];10size?: number;11trailSpeed?: number;12colorCycleSpeed?: number;13blur?: number;14pulseSpeed?: number;15pulseMin?: number;16pulseMax?: number;17}1819const RainbowCursor: React.FC<RainbowCursorProps> = ({20element,21length = 20,22colors = ['#FE0000', '#FD8C00', '#FFE500', '#119F0B', '#0644B3', '#C22EDC'],23size = 3,24trailSpeed = 0.4,25colorCycleSpeed = 0.002,26blur = 0,27pulseSpeed = 0.01,28pulseMin = 0.8,29pulseMax = 1.2,30}) => {31const canvasRef = useRef<HTMLCanvasElement | null>(null);32const contextRef = useRef<CanvasRenderingContext2D | null>(null);33const cursorRef = useRef({ x: 0, y: 0 });34const particlesRef = useRef<Array<{ position: { x: number; y: number } }>>(35[]36);37const animationFrameRef = useRef<number>();38const cursorsInittedRef = useRef(false);39const timeRef = useRef(0);4041class Particle {42position: { x: number; y: number };4344constructor(x: number, y: number) {45this.position = { x, y };46}47}4849// Helper function to interpolate between colors50const interpolateColors = (51color1: string,52color2: string,53factor: number54) => {55const r1 = parseInt(color1.substr(1, 2), 16);56const g1 = parseInt(color1.substr(3, 2), 16);57const b1 = parseInt(color1.substr(5, 2), 16);5859const r2 = parseInt(color2.substr(1, 2), 16);60const g2 = parseInt(color2.substr(3, 2), 16);61const b2 = parseInt(color2.substr(5, 2), 16);6263const r = Math.round(r1 + (r2 - r1) * factor);64const g = Math.round(g1 + (g2 - g1) * factor);65const b = Math.round(b1 + (b2 - b1) * factor);6667return `rgb(${r}, ${g}, ${b})`;68};6970// Function to get dynamic size based on pulse71const getPulseSize = (baseSize: number, time: number) => {72const pulse = Math.sin(time * pulseSpeed);73const scaleFactor = pulseMin + ((pulse + 1) * (pulseMax - pulseMin)) / 2;74return baseSize * scaleFactor;75};7677useEffect(() => {78const hasWrapperEl = element !== undefined;79const targetElement = hasWrapperEl ? element : document.body;8081const prefersReducedMotion = window.matchMedia(82'(prefers-reduced-motion: reduce)'83);8485if (prefersReducedMotion.matches) {86console.log('Reduced motion is enabled - cursor animation disabled');87return;88}8990const canvas = document.createElement('canvas');91const context = canvas.getContext('2d', { alpha: true });9293if (!context) return;9495canvasRef.current = canvas;96contextRef.current = context;9798canvas.style.top = '0px';99canvas.style.left = '0px';100canvas.style.pointerEvents = 'none';101canvas.style.position = hasWrapperEl ? 'absolute' : 'fixed';102103if (hasWrapperEl) {104element?.appendChild(canvas);105canvas.width = element.clientWidth;106canvas.height = element.clientHeight;107} else {108document.body.appendChild(canvas);109canvas.width = window.innerWidth;110canvas.height = window.innerHeight;111}112113const onMouseMove = (e: MouseEvent) => {114if (hasWrapperEl && element) {115const boundingRect = element.getBoundingClientRect();116cursorRef.current.x = e.clientX - boundingRect.left;117cursorRef.current.y = e.clientY - boundingRect.top;118} else {119cursorRef.current.x = e.clientX;120cursorRef.current.y = e.clientY;121}122123if (!cursorsInittedRef.current) {124cursorsInittedRef.current = true;125for (let i = 0; i < length; i++) {126particlesRef.current.push(127new Particle(cursorRef.current.x, cursorRef.current.y)128);129}130}131};132133const onWindowResize = () => {134if (hasWrapperEl && element) {135canvas.width = element.clientWidth;136canvas.height = element.clientHeight;137} else {138canvas.width = window.innerWidth;139canvas.height = window.innerHeight;140}141};142143const updateParticles = () => {144if (!contextRef.current || !canvasRef.current) return;145146const ctx = contextRef.current;147const canvas = canvasRef.current;148149ctx.clearRect(0, 0, canvas.width, canvas.height);150ctx.lineJoin = 'round';151152if (blur > 0) {153ctx.filter = `blur(${blur}px)`;154}155156const particleSets = [];157let x = cursorRef.current.x;158let y = cursorRef.current.y;159160particlesRef.current.forEach((particle, index) => {161const nextParticle =162particlesRef.current[index + 1] || particlesRef.current[0];163164particle.position.x = x;165particle.position.y = y;166167particleSets.push({ x, y });168169x += (nextParticle.position.x - particle.position.x) * trailSpeed;170y += (nextParticle.position.y - particle.position.y) * trailSpeed;171});172173// Time-based color cycling174timeRef.current += colorCycleSpeed;175const colorOffset = timeRef.current % 1;176177// Dynamic size based on pulse178const currentSize = getPulseSize(size, timeRef.current);179180colors.forEach((color, index) => {181const nextColor = colors[(index + 1) % colors.length];182183ctx.beginPath();184ctx.strokeStyle = interpolateColors(185color,186nextColor,187(index + colorOffset) / colors.length188);189190if (particleSets.length) {191ctx.moveTo(192particleSets[0].x,193particleSets[0].y + index * (currentSize - 1)194);195}196197particleSets.forEach((set, particleIndex) => {198if (particleIndex !== 0) {199ctx.lineTo(set.x, set.y + index * currentSize);200}201});202203ctx.lineWidth = currentSize;204ctx.lineCap = 'round';205ctx.stroke();206});207};208209const loop = () => {210updateParticles();211animationFrameRef.current = requestAnimationFrame(loop);212};213214targetElement.addEventListener('mousemove', onMouseMove);215window.addEventListener('resize', onWindowResize);216loop();217218return () => {219if (canvasRef.current) {220canvasRef.current.remove();221}222if (animationFrameRef.current) {223cancelAnimationFrame(animationFrameRef.current);224}225targetElement.removeEventListener('mousemove', onMouseMove);226window.removeEventListener('resize', onWindowResize);227};228}, [229element,230length,231colors,232size,233trailSpeed,234colorCycleSpeed,235blur,236pulseSpeed,237pulseMin,238pulseMax,239]);240241return null;242};243export default RainbowCursor;244
Prop | Type | Default | Description |
---|---|---|---|
element | HTMLElement | undefined | The HTML element where the cursor effect will be applied. Defaults to the entire document. |
length | number | 20 | The number of particles in the cursor trail. |
colors | string[] | ['#FE0000', '#FD8C00', '#FFE500', '#119F0B', '#0644B3', '#C22EDC'] | The array of colors for the cursor trail. |
size | number | 3 | The size of the particles in the cursor trail. |
trailSpeed | number | 0.4 | The speed at which the trail follows the cursor. |
colorCycleSpeed | number | 0.002 | The speed of the color transition for the trail. |
blur | number | 0 | The amount of blur applied to the trail. |
pulseSpeed | number | 0.01 | The speed of the pulsing effect for the particle size. |
pulseMin | number | 0.8 | The minimum size multiplier for the pulsing effect. |
pulseMax | number | 1.2 | The maximum size multiplier for the pulsing effect. |