Web InnoventixFreeCode

Spotlight Cursor Effect

Original · free

A cursor spotlight reveals a dimmed image.

byWeb InnoventixReact + Tailwind
spotlightcursorimageeffect
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-cursor.json
imgfx-spotlight-cursor.tsx
"use client";

import { useRef, type CSSProperties, type PointerEvent as ReactPointerEvent } from "react";
import { motion, useReducedMotion } from "motion/react";

export default function ImgfxSpotlightCursor() {
  const reduce = useReducedMotion();
  const stageRef = useRef<HTMLDivElement>(null);

  const move = (e: ReactPointerEvent<HTMLDivElement>) => {
    const el = stageRef.current;
    if (!el) return;
    const rect = el.getBoundingClientRect();
    el.style.setProperty("--scur-x", `${e.clientX - rect.left}px`);
    el.style.setProperty("--scur-y", `${e.clientY - rect.top}px`);
  };

  const enter = () => {
    stageRef.current?.style.setProperty("--scur-on", "1");
    stageRef.current?.style.setProperty("--scur-r", "clamp(150px, 22vw, 260px)");
  };

  const leave = () => {
    const el = stageRef.current;
    if (!el) return;
    el.style.setProperty("--scur-on", "0.22");
    el.style.setProperty("--scur-r", "clamp(90px, 14vw, 150px)");
    el.style.setProperty("--scur-x", "50%");
    el.style.setProperty("--scur-y", "42%");
  };

  const stageVars: CSSProperties = {
    "--scur-x": "50%",
    "--scur-y": "42%",
    "--scur-r": "clamp(90px, 14vw, 150px)",
    "--scur-on": "0.22",
  } as CSSProperties;

  const mask =
    "radial-gradient(circle var(--scur-r) at var(--scur-x) var(--scur-y), #000 0%, #000 52%, rgba(0,0,0,0.35) 74%, transparent 88%)";

  return (
    <section className="relative w-full overflow-hidden bg-neutral-100 px-5 py-20 sm:px-8 sm:py-28 dark:bg-neutral-950">
      <style>{`
        @property --scur-r {
          syntax: '<length>';
          inherits: true;
          initial-value: 120px;
        }
        .scur-stage {
          transition: --scur-r 0.55s cubic-bezier(0.22, 1, 0.36, 1);
        }
        .scur-reveal { transition: opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1); }
        @keyframes scur-drift {
          0%, 100% { transform: translate3d(0, 0, 0); }
          50% { transform: translate3d(0, -6px, 0); }
        }
        .scur-hintdot { animation: scur-drift 2.6s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .scur-stage { transition: none; }
          .scur-hintdot { animation: none; }
        }
      `}</style>

      {/* atmospheric backdrop */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 opacity-70"
        style={{
          background:
            "radial-gradient(60% 55% at 50% 0%, rgba(99,102,241,0.16), transparent 70%)",
        }}
      />

      <motion.div
        initial={reduce ? false : { opacity: 0, y: 28 }}
        whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
        viewport={{ once: true, margin: "-80px" }}
        transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1] }}
        className="relative mx-auto max-w-3xl"
      >
        <div className="mb-6 flex items-center gap-3">
          <span className="inline-block h-1.5 w-1.5 rounded-full bg-indigo-500" />
          <p className="text-xs font-semibold uppercase tracking-[0.28em] text-indigo-600 dark:text-indigo-400">
            Interactive · Spotlight
          </p>
        </div>

        <h2 className="max-w-xl text-3xl font-semibold leading-tight tracking-tight text-neutral-900 sm:text-4xl dark:text-neutral-50">
          Move your cursor to bring the frame to light.
        </h2>

        <div
          ref={stageRef}
          onPointerMove={move}
          onPointerEnter={enter}
          onPointerLeave={leave}
          style={stageVars}
          className="scur-stage group relative mt-8 aspect-[4/3] w-full cursor-none overflow-hidden rounded-2xl bg-neutral-900 ring-1 ring-neutral-900/10 dark:ring-white/10"
        >
          {/* dimmed base layer */}
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            src="/img/gallery/g14.webp"
            alt="Aerial view of a winding coastal road, shown dimmed until the spotlight passes over it"
            loading="lazy"
            draggable={false}
            className="absolute inset-0 h-full w-full scale-105 object-cover brightness-[0.34] contrast-[0.95] grayscale-[0.55] saturate-[0.7]"
          />

          {/* bright revealed layer, clipped to the spotlight */}
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            src="/img/gallery/g14.webp"
            alt=""
            aria-hidden
            loading="lazy"
            draggable={false}
            className="scur-reveal absolute inset-0 h-full w-full scale-105 object-cover"
            style={{
              opacity: "var(--scur-on)",
              WebkitMaskImage: mask,
              maskImage: mask,
            }}
          />

          {/* soft glow rim following the light */}
          <div
            aria-hidden
            className="pointer-events-none absolute inset-0 mix-blend-screen"
            style={{
              opacity: "var(--scur-on)",
              background:
                "radial-gradient(circle calc(var(--scur-r) * 1.15) at var(--scur-x) var(--scur-y), rgba(129,140,248,0.28), transparent 70%)",
            }}
          />

          {/* inner vignette for depth */}
          <div
            aria-hidden
            className="pointer-events-none absolute inset-0"
            style={{
              boxShadow: "inset 0 0 120px 24px rgba(0,0,0,0.55)",
            }}
          />

          {/* hint chip */}
          <div className="pointer-events-none absolute bottom-4 left-4 flex items-center gap-2 rounded-full bg-black/45 px-3.5 py-1.5 text-[11px] font-medium text-white/90 backdrop-blur-sm ring-1 ring-white/15">
            <span className="scur-hintdot inline-block h-1.5 w-1.5 rounded-full bg-emerald-400" />
            Drag your pointer across the frame
          </div>
        </div>

        <p className="mt-5 text-sm leading-relaxed text-neutral-500 dark:text-neutral-400">
          The photo stays in shadow until your cursor becomes the light source. On
          touch, a soft spotlight rests near the center of the frame.
        </p>
      </motion.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 →