Spotlight Text Reveal

An interactive cursor-based text reveal effect where a circular mask follows the mouse to uncover hidden content beneath the primary text. The spotlight dynamically expands on hover, creating a smooth and engaging reveal interaction. Designed for modern landing pages and storytelling sections, this component blends motion and masking techniques to produce an immersive, high-impact text experience.

framer-motion

Some mysteries are meant to be solved. Others are meant to remain mysteries.

When you have eliminated the impossible, whatever remains, however improbable, must be the truth.

Installation

Install the required dependencies:

npm install framer-motion
Link to required files
Follow this link and download the required files. - https://github.com/railav61/data/tree/main/forkui-data/spotlight-text-reveal-data
components/ui/spotlight-text-reveal.tsx
"use client";
import React, { useRef, useState } from "react";
import { motion } from "framer-motion";

type cardProps = {
  outerText?: string;
  innerText?: string;
  highlightText?: string;
};

function MaskCursorText({ outerText, innerText, highlightText }: cardProps) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [mousePosition, setmousePosition] = useState({ x: 0, y: 0 });
  const [isHovered, setIsHovered] = useState(false);
  const [size, setSize] = useState(40);

  const updateMousePosition = (e: React.MouseEvent<HTMLDivElement>) => {
    if (!containerRef.current) return;

    const rect = containerRef.current.getBoundingClientRect();

    setmousePosition({
      x: e.clientX - rect.left,
      y: e.clientY - rect.top,
    });
  };

  return (
    <div
      ref={containerRef}
      className="relative w-3xl h-full flex flex-col justify-center items-center text-[1.5rem] text-[#afa18f] font-extrabold"
    >
      <motion.div
        onMouseMove={updateMousePosition}
        className="absolute w-[60vw] top-[0%] h-full flex justify-center items-center bg-amber-400"
        animate={{
          // adjust distance b/w mask and cursor here just change numeric value like "-240 or -180" for x-axis similarly for y-axis
          maskPosition: `${mousePosition.x - (size + (isHovered ? -240 : -180))}px ${
            mousePosition.y - (size - (isHovered ? 100 : 30))
          }px`,
          maskSize: `${size}px`,
        }}
        transition={{
          type: "tween",
          ease: "backOut",
        }}
        style={{
          WebkitMaskImage: "url('/circle-mask.svg')",
          WebkitMaskRepeat: "no-repeat",
          WebkitMaskPosition: "50%",

          maskImage: "url('/circle-mask.svg')",
          maskRepeat: "no-repeat",
          maskPosition: "50%",
          color: "black",
        }}
      >
        <p
          className="w-[700px] h-fit cursor-default"
          onMouseEnter={() => {
            setIsHovered(true);
            setSize(200);
          }}
          onMouseLeave={() => {
            setIsHovered(false);
            setSize(40);
          }}
        >
          {innerText ||
            "Some mysteries are meant to be solved. Others are meant to remain mysteries."}
        </p>
      </motion.div>

      <div className="w-full h-screen flex justify-center items-center">
        <p className="w-[700px] h-fit cursor-default">
          When you have{" "}
          <span className="text-red-600">
            {highlightText || "eliminated the impossible,"}
          </span>
          {outerText ||
            " whatever remains, however improbable, must be the truth."}
        </p>
      </div>
    </div>
  );
}

export default MaskCursorText;

Usecase

Some mysteries are meant to be solved. Others are meant to remain mysteries.

When you have eliminated the impossible, whatever remains, however improbable, must be the truth.

* Simple example