Follow Cursor Effect

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

1
'use client';
2
3
import React, { useEffect } from 'react';
4
5
interface FollowCursorProps {
6
color?: string;
7
}
8
9
const FollowCursor: React.FC<FollowCursorProps> = ({ color = '#323232a6' }) => {
10
useEffect(() => {
11
let canvas: HTMLCanvasElement;
12
let context: CanvasRenderingContext2D | null;
13
let animationFrame: number;
14
let width = window.innerWidth;
15
let height = window.innerHeight;
16
let cursor = { x: width / 2, y: height / 2 };
17
const prefersReducedMotion = window.matchMedia(
18
'(prefers-reduced-motion: reduce)'
19
);
20
21
class Dot {
22
position: { x: number; y: number };
23
width: number;
24
lag: number;
25
26
constructor(x: number, y: number, width: number, lag: number) {
27
this.position = { x, y };
28
this.width = width;
29
this.lag = lag;
30
}
31
32
moveTowards(x: number, y: number, context: CanvasRenderingContext2D) {
33
this.position.x += (x - this.position.x) / this.lag;
34
this.position.y += (y - this.position.y) / this.lag;
35
context.fillStyle = color;
36
context.beginPath();
37
context.arc(
38
this.position.x,
39
this.position.y,
40
this.width,
41
0,
42
2 * Math.PI
43
);
44
context.fill();
45
context.closePath();
46
}
47
}
48
49
const dot = new Dot(width / 2, height / 2, 10, 10);
50
51
const onMouseMove = (e: MouseEvent) => {
52
cursor.x = e.clientX;
53
cursor.y = e.clientY;
54
};
55
56
const onWindowResize = () => {
57
width = window.innerWidth;
58
height = window.innerHeight;
59
if (canvas) {
60
canvas.width = width;
61
canvas.height = height;
62
}
63
};
64
65
const updateDot = () => {
66
if (context) {
67
context.clearRect(0, 0, width, height);
68
dot.moveTowards(cursor.x, cursor.y, context);
69
}
70
};
71
72
const loop = () => {
73
updateDot();
74
animationFrame = requestAnimationFrame(loop);
75
};
76
77
const init = () => {
78
if (prefersReducedMotion.matches) {
79
console.log('Reduced motion enabled, cursor effect skipped.');
80
return;
81
}
82
83
canvas = document.createElement('canvas');
84
context = canvas.getContext('2d');
85
canvas.style.position = 'fixed';
86
canvas.style.top = '0';
87
canvas.style.left = '0';
88
canvas.style.pointerEvents = 'none';
89
canvas.width = width;
90
canvas.height = height;
91
document.body.appendChild(canvas);
92
93
window.addEventListener('mousemove', onMouseMove);
94
window.addEventListener('resize', onWindowResize);
95
loop();
96
};
97
98
const destroy = () => {
99
if (canvas) canvas.remove();
100
cancelAnimationFrame(animationFrame);
101
window.removeEventListener('mousemove', onMouseMove);
102
window.removeEventListener('resize', onWindowResize);
103
};
104
105
prefersReducedMotion.onchange = () => {
106
if (prefersReducedMotion.matches) {
107
destroy();
108
} else {
109
init();
110
}
111
};
112
113
init();
114
115
return () => {
116
destroy();
117
};
118
}, [color]);
119
120
return null; // This component doesn't render any visible JSX
121
};
122
123
export default FollowCursor;
124