Rainbow Cursor Effect
An interactive React component that adds a dynamic rainbow effect, visually tracking cursor movement in real time.
An interactive React component that adds a dynamic rainbow 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;17zIndex?: number;18}1920const RainbowCursor: React.FC<RainbowCursorProps> = ({21element,22length = 20,23colors = ['#FE0000', '#FD8C00', '#FFE500', '#119F0B', '#0644B3', '#C22EDC'],24size = 3,25trailSpeed = 0.4,26colorCycleSpeed = 0.002,27blur = 0,28pulseSpeed = 0.01,29pulseMin = 0.8,30pulseMax = 1.2,31zIndex32}) => {33const canvasRef = useRef<HTMLCanvasElement | null>(null);34const contextRef = useRef<CanvasRenderingContext2D | null>(null);35const cursorRef = useRef({ x: 0, y: 0 });36const particlesRef = useRef<Array<{ position: { x: number; y: number } }>>(37[]38);39const animationFrameRef = useRef<number>(undefined);40const cursorsInittedRef = useRef(false);41const timeRef = useRef(0);4243class Particle {44position: { x: number; y: number };4546constructor(x: number, y: number) {47this.position = { x, y };48}49}5051// Helper function to interpolate between colors52const interpolateColors = (53color1: string,54color2: string,55factor: number56) => {57const r1 = parseInt(color1.substr(1, 2), 16);58const g1 = parseInt(color1.substr(3, 2), 16);59const b1 = parseInt(color1.substr(5, 2), 16);6061const r2 = parseInt(color2.substr(1, 2), 16);62const g2 = parseInt(color2.substr(3, 2), 16);63const b2 = parseInt(color2.substr(5, 2), 16);6465const r = Math.round(r1 + (r2 - r1) * factor);66const g = Math.round(g1 + (g2 - g1) * factor);67const b = Math.round(b1 + (b2 - b1) * factor);6869return `rgb(${r}, ${g}, ${b})`;70};7172// Function to get dynamic size based on pulse73const getPulseSize = (baseSize: number, time: number) => {74const pulse = Math.sin(time * pulseSpeed);75const scaleFactor = pulseMin + ((pulse + 1) * (pulseMax - pulseMin)) / 2;76return baseSize * scaleFactor;77};7879useEffect(() => {80const hasWrapperEl = element !== undefined;81const targetElement = hasWrapperEl ? element : document.body;8283const prefersReducedMotion = window.matchMedia(84'(prefers-reduced-motion: reduce)'85);8687if (prefersReducedMotion.matches) {88console.log('Reduced motion is enabled - cursor animation disabled');89return;90}9192const canvas = document.createElement('canvas');93const context = canvas.getContext('2d', { alpha: true });9495if (!context) return;9697canvasRef.current = canvas;98contextRef.current = context;99100canvas.style.top = '0px';101canvas.style.left = '0px';102canvas.style.pointerEvents = 'none';103canvas.style.position = hasWrapperEl ? 'absolute' : 'fixed';104canvas.style.zIndex = zIndex ? zIndex.toString() : '';105106if (hasWrapperEl) {107element?.appendChild(canvas);108canvas.width = element.clientWidth;109canvas.height = element.clientHeight;110} else {111document.body.appendChild(canvas);112canvas.width = window.innerWidth;113canvas.height = window.innerHeight;114}115116const onMouseMove = (e: MouseEvent) => {117if (hasWrapperEl && element) {118const boundingRect = element.getBoundingClientRect();119cursorRef.current.x = e.clientX - boundingRect.left;120cursorRef.current.y = e.clientY - boundingRect.top;121} else {122cursorRef.current.x = e.clientX;123cursorRef.current.y = e.clientY;124}125126if (!cursorsInittedRef.current) {127cursorsInittedRef.current = true;128for (let i = 0; i < length; i++) {129particlesRef.current.push(130new Particle(cursorRef.current.x, cursorRef.current.y)131);132}133}134};135136const onWindowResize = () => {137if (hasWrapperEl && element) {138canvas.width = element.clientWidth;139canvas.height = element.clientHeight;140} else {141canvas.width = window.innerWidth;142canvas.height = window.innerHeight;143}144};145146const updateParticles = () => {147if (!contextRef.current || !canvasRef.current) return;148149const ctx = contextRef.current;150const canvas = canvasRef.current;151152ctx.clearRect(0, 0, canvas.width, canvas.height);153ctx.lineJoin = 'round';154155if (blur > 0) {156ctx.filter = `blur(${blur}px)`;157}158159const particleSets = [];160let x = cursorRef.current.x;161let y = cursorRef.current.y;162163particlesRef.current.forEach((particle, index) => {164const nextParticle =165particlesRef.current[index + 1] || particlesRef.current[0];166167particle.position.x = x;168particle.position.y = y;169170particleSets.push({ x, y });171172x += (nextParticle.position.x - particle.position.x) * trailSpeed;173y += (nextParticle.position.y - particle.position.y) * trailSpeed;174});175176// Time-based color cycling177timeRef.current += colorCycleSpeed;178const colorOffset = timeRef.current % 1;179180// Dynamic size based on pulse181const currentSize = getPulseSize(size, timeRef.current);182183colors.forEach((color, index) => {184const nextColor = colors[(index + 1) % colors.length];185186ctx.beginPath();187ctx.strokeStyle = interpolateColors(188color,189nextColor,190(index + colorOffset) / colors.length191);192193if (particleSets.length) {194ctx.moveTo(195particleSets[0].x,196particleSets[0].y + index * (currentSize - 1)197);198}199200particleSets.forEach((set, particleIndex) => {201if (particleIndex !== 0) {202ctx.lineTo(set.x, set.y + index * currentSize);203}204});205206ctx.lineWidth = currentSize;207ctx.lineCap = 'round';208ctx.stroke();209});210};211212const loop = () => {213updateParticles();214animationFrameRef.current = requestAnimationFrame(loop);215};216217targetElement.addEventListener('mousemove', onMouseMove);218window.addEventListener('resize', onWindowResize);219loop();220221return () => {222if (canvasRef.current) {223canvasRef.current.remove();224}225if (animationFrameRef.current) {226cancelAnimationFrame(animationFrameRef.current);227}228targetElement.removeEventListener('mousemove', onMouseMove);229window.removeEventListener('resize', onWindowResize);230};231}, [232element,233length,234colors,235size,236trailSpeed,237colorCycleSpeed,238blur,239pulseSpeed,240pulseMin,241pulseMax,242]);243244return null;245};246export default RainbowCursor;247
| 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. |