motion-primitives
components
ui
animation
motion

spotlight

A dynamic spotlight effect component that follows cursor movement.

animated
effect
form
gradient
hover
list
motion
positioning
tailwind
transform
transition
View Docs

Source Code

Files
spotlight.tsx
1'use client';
2import React, { useRef, useState, useCallback, useEffect } from 'react';
3import { motion, useSpring, useTransform, SpringOptions } from 'motion/react';
4import { cn } from '@/lib/utils';
5
6export type SpotlightProps = {
7  className?: string;
8  size?: number;
9  springOptions?: SpringOptions;
10};
11
12export function Spotlight({
13  className,
14  size = 200,
15  springOptions = { bounce: 0 },
16}: SpotlightProps) {
17  const containerRef = useRef<HTMLDivElement>(null);
18  const [isHovered, setIsHovered] = useState(false);
19  const [parentElement, setParentElement] = useState<HTMLElement | null>(null);
20
21  const mouseX = useSpring(0, springOptions);
22  const mouseY = useSpring(0, springOptions);
23
24  const spotlightLeft = useTransform(mouseX, (x) => `${x - size / 2}px`);
25  const spotlightTop = useTransform(mouseY, (y) => `${y - size / 2}px`);
26
27  useEffect(() => {
28    if (containerRef.current) {
29      const parent = containerRef.current.parentElement;
30      if (parent) {
31        parent.style.position = 'relative';
32        parent.style.overflow = 'hidden';
33        setParentElement(parent);
34      }
35    }
36  }, []);
37
38  const handleMouseMove = useCallback(
39    (event: MouseEvent) => {
40      if (!parentElement) return;
41      const { left, top } = parentElement.getBoundingClientRect();
42      mouseX.set(event.clientX - left);
43      mouseY.set(event.clientY - top);
44    },
45    [mouseX, mouseY, parentElement]
46  );
47
48  useEffect(() => {
49    if (!parentElement) return;
50
51    parentElement.addEventListener('mousemove', handleMouseMove);
52    parentElement.addEventListener('mouseenter', () => setIsHovered(true));
53    parentElement.addEventListener('mouseleave', () => setIsHovered(false));
54
55    return () => {
56      parentElement.removeEventListener('mousemove', handleMouseMove);
57      parentElement.removeEventListener('mouseenter', () => setIsHovered(true));
58      parentElement.removeEventListener('mouseleave', () =>
59        setIsHovered(false)
60      );
61    };
62  }, [parentElement, handleMouseMove]);
63
64  return (
65    <motion.div
66      ref={containerRef}
67      className={cn(
68        'pointer-events-none absolute rounded-full bg-[radial-gradient(circle_at_center,var(--tw-gradient-stops),transparent_80%)] blur-xl transition-opacity duration-200',
69        'from-zinc-50 via-zinc-100 to-zinc-200',
70        isHovered ? 'opacity-100' : 'opacity-0',
71        className
72      )}
73      style={{
74        width: size,
75        height: size,
76        left: spotlightLeft,
77        top: spotlightTop,
78      }}
79    />
80  );
81}
82