Follow Cursor Effect

An interactive React component that tracks the cursor with a small gray circle, creating a smooth following effect.

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