Spotlight Cursor Effect

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

1
'use client';
2
import { HTMLAttributes } from 'react';
3
import useSpotlightEffect from '@/hooks/use-spotlight';
4
5
// Define an interface for the spotlight configuration
6
interface SpotlightConfig {
7
radius?: number;
8
brightness?: number;
9
color?: string;
10
smoothing?: number;
11
}
12
13
// Combine props with potential HTML canvas attributes
14
interface SpotlightCursorProps extends HTMLAttributes<HTMLCanvasElement> {
15
config?: SpotlightConfig;
16
}
17
18
const SpotlightCursor = ({
19
config = {},
20
className,
21
...rest
22
}: SpotlightCursorProps) => {
23
// Provide default configuration if not specified
24
const spotlightConfig = {
25
radius: 200,
26
brightness: 0.15,
27
color: '#ffffff',
28
smoothing: 0.1,
29
...config,
30
};
31
32
const canvasRef = useSpotlightEffect(spotlightConfig);
33
34
return (
35
<canvas
36
ref={canvasRef}
37
className={`fixed top-0 left-0 pointer-events-none z-[9999] w-full h-full ${className}`}
38
{...rest}
39
/>
40
);
41
};
42
43
export default SpotlightCursor;
44

useSpotlightEffect

hooks/useSpotlightEffect.tsx
1
// @ts-nocheck
2
'use client';
3
import { useEffect, useRef, useState } from 'react';
4
5
const useSpotlightEffect = (config = {}) => {
6
const {
7
spotlightSize = 200,
8
spotlightIntensity = 0.8,
9
fadeSpeed = 0.1,
10
glowColor = '255, 255, 255',
11
pulseSpeed = 2000,
12
} = config;
13
14
const canvasRef = useRef(null);
15
const ctxRef = useRef(null);
16
const spotlightPos = useRef({ x: 0, y: 0 });
17
const targetPos = useRef({ x: 0, y: 0 });
18
const animationFrame = useRef(null);
19
const [isHovered, setIsHovered] = useState(false);
20
21
useEffect(() => {
22
const canvas = canvasRef.current;
23
const ctx = canvas.getContext('2d');
24
ctxRef.current = ctx;
25
26
const resizeCanvas = () => {
27
canvas.width = window.innerWidth;
28
canvas.height = window.innerHeight;
29
};
30
31
const lerp = (start, end, factor) => {
32
return start + (end - start) * factor;
33
};
34
35
const handleMouseMove = (e) => {
36
targetPos.current = { x: e.clientX, y: e.clientY };
37
setIsHovered(true);
38
};
39
40
const handleMouseLeave = () => {
41
setIsHovered(false);
42
};
43
44
const render = () => {
45
if (!canvas || !ctx) return;
46
47
// Smooth position transition
48
spotlightPos.current.x = lerp(
49
spotlightPos.current.x,
50
targetPos.current.x,
51
fadeSpeed
52
);
53
spotlightPos.current.y = lerp(
54
spotlightPos.current.y,
55
targetPos.current.y,
56
fadeSpeed
57
);
58
59
ctx.clearRect(0, 0, canvas.width, canvas.height);
60
61
// Create dark overlay
62
ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
63
ctx.fillRect(0, 0, canvas.width, canvas.height);
64
65
// Calculate pulse effect
66
const pulseScale =
67
1 + 0.1 * Math.sin((Date.now() / pulseSpeed) * Math.PI * 2);
68
const currentSpotlightSize = spotlightSize * pulseScale;
69
70
// Create spotlight gradient
71
const gradient = ctx.createRadialGradient(
72
spotlightPos.current.x,
73
spotlightPos.current.y,
74
0,
75
spotlightPos.current.x,
76
spotlightPos.current.y,
77
currentSpotlightSize
78
);
79
80
// Add multiple color stops for smoother transition
81
gradient.addColorStop(0, `rgba(${glowColor}, ${spotlightIntensity})`);
82
gradient.addColorStop(
83
0.5,
84
`rgba(${glowColor}, ${spotlightIntensity * 0.5})`
85
);
86
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
87
88
// Apply spotlight effect
89
ctx.globalCompositeOperation = 'destination-out';
90
ctx.fillStyle = gradient;
91
ctx.beginPath();
92
ctx.arc(
93
spotlightPos.current.x,
94
spotlightPos.current.y,
95
currentSpotlightSize,
96
0,
97
Math.PI * 2
98
);
99
ctx.fill();
100
101
// Add glow effect
102
ctx.globalCompositeOperation = 'source-over';
103
const glowGradient = ctx.createRadialGradient(
104
spotlightPos.current.x,
105
spotlightPos.current.y,
106
0,
107
spotlightPos.current.x,
108
spotlightPos.current.y,
109
currentSpotlightSize * 1.2
110
);
111
glowGradient.addColorStop(0, `rgba(${glowColor}, 0.2)`);
112
glowGradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
113
ctx.fillStyle = glowGradient;
114
ctx.beginPath();
115
ctx.arc(
116
spotlightPos.current.x,
117
spotlightPos.current.y,
118
currentSpotlightSize * 1.2,
119
0,
120
Math.PI * 2
121
);
122
ctx.fill();
123
124
animationFrame.current = requestAnimationFrame(render);
125
};
126
127
resizeCanvas();
128
window.addEventListener('resize', resizeCanvas);
129
document.addEventListener('mousemove', handleMouseMove);
130
document.addEventListener('mouseleave', handleMouseLeave);
131
render();
132
133
return () => {
134
window.removeEventListener('resize', resizeCanvas);
135
document.addEventListener('mousemove', handleMouseMove);
136
document.removeEventListener('mouseleave', handleMouseLeave);
137
if (animationFrame.current) {
138
cancelAnimationFrame(animationFrame.current);
139
}
140
};
141
}, [spotlightSize, spotlightIntensity, fadeSpeed, glowColor, pulseSpeed]);
142
143
return canvasRef;
144
};
145
146
export default useSpotlightEffect;

Props

PropTypeDefaultDescription
config{ radius?: number, brightness?: number, color?: string, smoothing?: number }{}Configuration object for the spotlight cursor.
radiusnumberundefinedThe radius of the spotlight effect in pixels.
brightnessnumberundefinedThe brightness of the spotlight effect.
colorstringundefinedThe color of the spotlight.
smoothingnumberundefinedThe smoothing factor applied to the spotlight effect (a value between 0 and 1).
classNamestringundefinedAdditional class names to apply to the canvas element.