Web InnoventixFreeCode

Spotlight Follow Effect

Original · free

A radial spotlight follows the cursor across the image.

byWeb InnoventixReact + Tailwind
spotlightfollowimageeffect
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/imgfx-spotlight-follow.json
imgfx-spotlight-follow.tsx
"use client";

import { useRef, useState, type MouseEvent } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";

export default function ImgfxSpotlightFollow() {
  const stageRef = useRef<HTMLDivElement>(null);
  const sectionRef = useRef<HTMLDivElement>(null);
  const inView = useInView(sectionRef, { once: true, margin: "-80px" });
  const reduce = useReducedMotion();
  const [lit, setLit] = useState(false);

  const handleMove = (e: MouseEvent<HTMLDivElement>) => {
    const el = stageRef.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    const x = ((e.clientX - rect.left) / rect.width) * 100;
    const y = ((e.clientY - rect.top) / rect.height) * 100;
    el.style.setProperty("--sx", `${x}%`);
    el.style.setProperty("--sy", `${y}%`);
  };

  return (
    <section className="relative w-full overflow-hidden bg-zinc-50 px-6 py-20 dark:bg-zinc-950 sm:px-10 sm:py-28">
      <style>{`
        @keyframes imgfx_sf_hint {
          0%, 100% { opacity: 0.55; transform: scale(1); }
          50%      { opacity: 0.85; transform: scale(1.06); }
        }
        .imgfx_sf_idlebeam { animation: imgfx_sf_hint 3.4s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .imgfx_sf_idlebeam { animation: none; }
        }
      `}</style>

      <div ref={sectionRef} className="mx-auto flex max-w-3xl flex-col items-center">
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 24 }}
          animate={inView && !reduce ? { opacity: 1, y: 0 } : undefined}
          transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
          className="w-full text-center"
        >
          <p className="text-xs font-semibold uppercase tracking-[0.28em] text-indigo-600 dark:text-indigo-400">
            Image Effect
          </p>
          <h2 className="mt-3 text-3xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50 sm:text-4xl">
            Spotlight Follow
          </h2>
          <p className="mx-auto mt-4 max-w-md text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
            The photo rests in shadow until your cursor brings it to life. A soft
            radial beam tracks your pointer and reveals colour, one circle at a time.
          </p>
        </motion.div>

        <motion.div
          initial={reduce ? false : { opacity: 0, scale: 0.96 }}
          animate={inView && !reduce ? { opacity: 1, scale: 1 } : undefined}
          transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1], delay: 0.1 }}
          className="mt-10 w-full"
        >
          <div
            ref={stageRef}
            onMouseMove={handleMove}
            onMouseEnter={() => setLit(true)}
            onMouseLeave={() => setLit(false)}
            style={{ ["--sx" as string]: "50%", ["--sy" as string]: "42%" }}
            className="group relative aspect-[4/3] w-full cursor-crosshair overflow-hidden rounded-2xl border border-zinc-200 bg-zinc-900 shadow-xl ring-1 ring-black/5 dark:border-zinc-800 dark:shadow-2xl dark:ring-white/5"
          >
            {/* Base layer: dimmed, desaturated resting state */}
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src="/img/gallery/g14.webp"
              alt="Mountain landscape shown in muted shadow"
              loading="lazy"
              draggable={false}
              className="absolute inset-0 h-full w-full select-none object-cover brightness-[0.32] grayscale transition duration-500"
            />

            {/* Reveal layer: full colour, masked to the spotlight around the cursor */}
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src="/img/gallery/g14.webp"
              alt=""
              aria-hidden="true"
              loading="lazy"
              draggable={false}
              className="absolute inset-0 h-full w-full select-none object-cover transition-opacity duration-500 will-change-[opacity]"
              style={{
                opacity: lit ? 1 : 0,
                WebkitMaskImage:
                  "radial-gradient(circle 170px at var(--sx) var(--sy), #000 0%, #000 42%, transparent 74%)",
                maskImage:
                  "radial-gradient(circle 170px at var(--sx) var(--sy), #000 0%, #000 42%, transparent 74%)",
              }}
            />

            {/* Warm glow that rides the beam edge */}
            <div
              aria-hidden="true"
              className="pointer-events-none absolute inset-0 transition-opacity duration-500"
              style={{
                opacity: lit ? 0.9 : 0,
                background:
                  "radial-gradient(circle 190px at var(--sx) var(--sy), rgba(129,140,248,0.28) 0%, rgba(129,140,248,0.10) 45%, transparent 72%)",
                mixBlendMode: "screen",
              }}
            />

            {/* Idle hint beam, shown only before the first hover */}
            <div
              aria-hidden="true"
              className={`imgfx_sf_idlebeam pointer-events-none absolute inset-0 transition-opacity duration-500 ${
                lit ? "opacity-0" : "opacity-100"
              }`}
              style={{
                background:
                  "radial-gradient(circle 150px at 50% 42%, rgba(255,255,255,0.14) 0%, transparent 70%)",
              }}
            />

            {/* Inner vignette for depth */}
            <div
              aria-hidden="true"
              className="pointer-events-none absolute inset-0 rounded-2xl shadow-[inset_0_0_120px_40px_rgba(0,0,0,0.55)]"
            />

            <span className="pointer-events-none absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full bg-white/10 px-3 py-1 text-[11px] font-medium tracking-wide text-white/80 backdrop-blur-sm transition-opacity duration-300 group-hover:opacity-0">
              Move your cursor to reveal
            </span>
          </div>
        </motion.div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →