Smooth Follower Cursor

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

1
"use client"
2
3
import { useState, useEffect, useRef } from "react"
4
5
export default function SmoothFollower() {
6
const mousePosition = useRef({ x: 0, y: 0 })
7
8
const dotPosition = useRef({ x: 0, y: 0 })
9
const borderDotPosition = useRef({ x: 0, y: 0 })
10
11
const [renderPos, setRenderPos] = useState({ dot: { x: 0, y: 0 }, border: { x: 0, y: 0 } })
12
const [isHovering, setIsHovering] = useState(false)
13
14
const DOT_SMOOTHNESS = 0.2
15
const BORDER_DOT_SMOOTHNESS = 0.1
16
17
useEffect(() => {
18
const handleMouseMove = (e: MouseEvent) => {
19
mousePosition.current = { x: e.clientX, y: e.clientY }
20
}
21
22
const handleMouseEnter = () => setIsHovering(true)
23
const handleMouseLeave = () => setIsHovering(false)
24
25
// Add event listeners
26
window.addEventListener("mousemove", handleMouseMove)
27
28
const interactiveElements = document.querySelectorAll("a, button, img, input, textarea, select")
29
interactiveElements.forEach((element) => {
30
element.addEventListener("mouseenter", handleMouseEnter)
31
element.addEventListener("mouseleave", handleMouseLeave)
32
})
33
34
// Animation function for smooth movement
35
const animate = () => {
36
const lerp = (start: number, end: number, factor: number) => {
37
return start + (end - start) * factor
38
}
39
40
dotPosition.current.x = lerp(dotPosition.current.x, mousePosition.current.x, DOT_SMOOTHNESS)
41
dotPosition.current.y = lerp(dotPosition.current.y, mousePosition.current.y, DOT_SMOOTHNESS)
42
43
borderDotPosition.current.x = lerp(borderDotPosition.current.x, mousePosition.current.x, BORDER_DOT_SMOOTHNESS)
44
borderDotPosition.current.y = lerp(borderDotPosition.current.y, mousePosition.current.y, BORDER_DOT_SMOOTHNESS)
45
46
setRenderPos({
47
dot: { x: dotPosition.current.x, y: dotPosition.current.y },
48
border: { x: borderDotPosition.current.x, y: borderDotPosition.current.y },
49
})
50
51
requestAnimationFrame(animate)
52
}
53
54
// Start animation loop
55
const animationId = requestAnimationFrame(animate)
56
57
// Clean up
58
return () => {
59
window.removeEventListener("mousemove", handleMouseMove)
60
61
interactiveElements.forEach((element) => {
62
element.removeEventListener("mouseenter", handleMouseEnter)
63
element.removeEventListener("mouseleave", handleMouseLeave)
64
})
65
66
cancelAnimationFrame(animationId)
67
}
68
}, [])
69
70
if (typeof window === "undefined") return null
71
72
return (
73
<div className="pointer-events-none fixed inset-0 z-50">
74
<div
75
className="absolute rounded-full dark:bg-white bg-black "
76
style={{
77
width: "8px",
78
height: "8px",
79
transform: "translate(-50%, -50%)",
80
left: `${renderPos.dot.x}px`,
81
top: `${renderPos.dot.y}px`,
82
}}
83
/>
84
85
<div
86
className="absolute rounded-full border dark:border-white border-black "
87
style={{
88
width: isHovering ? "44px" : "28px",
89
height: isHovering ? "44px" : "28px",
90
transform: "translate(-50%, -50%)",
91
left: `${renderPos.border.x}px`,
92
top: `${renderPos.border.y}px`,
93
transition: "width 0.3s, height 0.3s",
94
}}
95
/>
96
</div>
97
)
98
}
99