A cursor-following spotlight effect that reveals a soft glowing circle wherever the user moves their mouse inside a container.
Made by LéoFast & Lightweight
Built with performance in mind, keeping your bundle size minimal.

"use client";
import { ClippedCircle } from "@/components/unlumen-ui/clipped-circle";
import { Button } from "@/components/ui/button";
import Image from "next/image";
interface ClippedCircleDemoProps {
circleSize: number;
}
export default function ClippedCircleDemo({
circleSize = 400,
}: ClippedCircleDemoProps) {
return (
<div className="flex flex-wrap gap-8 items-center justify-center p-8">
{/* Button */}
<Button variant="outline" className="overflow-hidden relative group">
Get Started
<ClippedCircle circleSize={300} circleClassName="bg-white" />
</Button>
{/* Card */}
<div className="relative text-background border aspect-square overflow-hidden rounded-xl bg-primary p-6 w-64 cursor-default">
<h3 className="font-medium text-3xl mb-1">Fast & Lightweight</h3>
<p className="text-background/50 text-xs leading-relaxed">
Built with performance in mind, keeping your bundle size minimal.
</p>
<ClippedCircle circleSize={circleSize} circleClassName="bg-white" />
</div>
{/* Image */}
<div className="relative border-5 overflow-hidden rounded-xl cursor-default">
<Image
src="/web-app-manifest-512x512.png"
width={200}
height={200}
alt="Clipped Circle Demo"
className="w-full h-full object-cover"
/>
<ClippedCircle circleSize={circleSize} circleClassName="bg-white" />
</div>
</div>
);
}Installation
Install the following dependencies:
Copy and paste the following code into your project:
"use client";
import * as React from "react";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface ClippedCircleProps {
className?: string;
circleClassName?: string;
circleSize?: number;
}
function ClippedCircle({
className,
circleClassName = "bg-white/20",
circleSize = 400,
}: ClippedCircleProps) {
const containerRef = React.useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = React.useState(false);
const [position, setPosition] = React.useState({ x: "50%", y: "50%" });
React.useEffect(() => {
const container = containerRef.current;
if (!container || !container.parentElement) return;
const parent = container.parentElement;
const handleMouseEnter = (e: MouseEvent) => {
const rect = parent.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
setPosition({ x: `${x}%`, y: `${y}%` });
setIsHovered(true);
};
const handleMouseMove = (e: MouseEvent) => {
const rect = parent.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
setPosition({ x: `${x}%`, y: `${y}%` });
};
const handleMouseLeave = () => {
setIsHovered(false);
};
parent.addEventListener("mouseenter", handleMouseEnter);
parent.addEventListener("mousemove", handleMouseMove);
parent.addEventListener("mouseleave", handleMouseLeave);
return () => {
parent.removeEventListener("mouseenter", handleMouseEnter);
parent.removeEventListener("mousemove", handleMouseMove);
parent.removeEventListener("mouseleave", handleMouseLeave);
};
}, []);
return (
<div
ref={containerRef}
className={cn(
"absolute inset-0 overflow-hidden pointer-events-none",
className,
)}
>
<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>
);
}
export { ClippedCircle, type ClippedCircleProps };Update the import paths to match your project setup.
Usage
Place ClippedCircle as a direct child of any relative overflow-hidden container. It attaches mouse listeners to its parent element automatically.
import { ClippedCircle } from "@/components/unlumen-ui/components/effects/clipped-circle";
// Inside a button
<button className="relative overflow-hidden ...">
Click me
<ClippedCircle circleSize={300} circleClassName="bg-white/25" />
</button>
// Inside a card
<div className="relative overflow-hidden ...">
<p>Card content</p>
<ClippedCircle circleSize={500} circleClassName="bg-violet-500/30" />
</div>API Reference
ClippedCircle
| Prop | Type | Default |
|---|---|---|
circleSize? | number | 400 |
circleClassName? | string | "bg-white/20" |
className? | string | - |