Spotlight Follow
A cursor-following spotlight in difference blend: drop it inside any relatively-positioned section and a soft circle chases the pointer, inverting whatever it passes over. It binds its listeners to the parent element, so the host section needs no wiring beyond position: relative.
Micro InteractionReactMotionTailwind CSS
CSSTailwind
Manual
Create a file and paste the following code into it.
spotlight-follow.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
"use client";
import { useEffect, useRef, useState } from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/cn";
/* ------------------------------------------------------------------ */
/* SpotlightFollow */
/* ------------------------------------------------------------------ */
interface SpotlightFollowProps {
className?: string;
/** Classes for the circle itself (color/opacity) [Optional] */
circleClassName?: string;
/** Circle diameter in px [Optional, default: 400] */
circleSize?: number;
}
/**
* A cursor-following spotlight in difference blend: drop it inside any
* relatively-positioned section and a soft circle chases the pointer,
* inverting whatever it passes over. It binds its listeners to the PARENT
* element, so the host section needs no wiring — just position: relative.
* @param {number} circleSize - Spotlight diameter [Optional, default: 400]
*/
export function SpotlightFollow({
className,
circleClassName = "bg-white/20",
circleSize = 400,
}: SpotlightFollowProps) {
const hostRef = useRef<HTMLDivElement | null>(null);
const [isHovered, setIsHovered] = useState(false);
const [position, setPosition] = useState({ x: "50%", y: "50%" });
useEffect(() => {
const parent = hostRef.current?.parentElement;
if (!parent) return;
const track = (e: MouseEvent) => {
const rect = parent.getBoundingClientRect();
setPosition({
x: `${((e.clientX - rect.left) / rect.width) * 100}%`,
y: `${((e.clientY - rect.top) / rect.height) * 100}%`,
});
};
const enter = (e: MouseEvent) => {
track(e);
setIsHovered(true);
};
const leave = () => setIsHovered(false);
parent.addEventListener("mouseenter", enter);
parent.addEventListener("mousemove", track);
parent.addEventListener("mouseleave", leave);
return () => {
parent.removeEventListener("mouseenter", enter);
parent.removeEventListener("mousemove", track);
parent.removeEventListener("mouseleave", leave);
};
}, []);
return (
<div
ref={hostRef}
className={cn("pointer-events-none absolute inset-0 overflow-hidden", className)}
aria-hidden
>
<motion.div
className={cn("pointer-events-none absolute rounded-full", circleClassName)}
style={{
left: position.x,
top: position.y,
width: circleSize,
height: circleSize,
mixBlendMode: "difference",
}}
initial={{ scale: 0, x: "-50%", y: "-50%" }}
animate={{ scale: isHovered ? 1 : 0, x: "-50%", y: "-50%" }}
transition={{ duration: 0.5, ease: [0.19, 1, 0.22, 1] }}
/>
</div>
);
}
Update the import paths to match your project setup.
Similar components
Install via CLI
Resource details
PublishedJuly 17, 2026
CategoryMicro Interaction
ReactMotionTailwind CSS
Install via CLI
Resource details
PublishedJuly 17, 2026
CategoryMicro Interaction
ReactMotionTailwind CSS