Mask Reveal on Hover

Reveals alternate content through a circular mask that follows your cursor. The base content is shown dimmed; hovering grows the mask to reveal the overlay content. Uses CSS mask-image and GSAP for smooth position and size animations.

React
Tailwind CSS
GSAP
Interactive
Mask
CSS

Writing beautiful code means thinking like an artist and debugging like a detective. Every function is a story, every variable a character. Master your craft through practice, patience, and endless curiosity about how things work.

Building great software requires seeing beyond syntax into architecture and design. Test early, refactor often, document clearly. Success comes from collaboration, continuous learning, and caring deeply about user experience.

Using CLI

npx dimaac add MaskRevealOnHover

Manual Installation

npm install react @gsap/react

lib/utils.ts

import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
} 

components/ui/interactive/MaskRevealOnHover.tsx

'use client';

import React, { useRef } from 'react';
import { gsap } from 'gsap';
import { useGSAP } from '@gsap/react';
import { cn } from '@/lib/utils';

interface MaskRevealOnHoverProps {
  originalContent: React.ReactNode;
  maskContent: React.ReactNode;
  maskSizeSmall?: number;
  maskSizeLarge?: number;
  maskBackground?: string;
  className?: string;
}

const MaskRevealOnHover: React.FC<MaskRevealOnHoverProps> = ({
  originalContent,
  maskContent,
  maskSizeSmall = 20,
  maskSizeLarge = 100,
  maskBackground = '#DDFC3E',
  className,
}) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const maskRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);

  useGSAP(
    () => {
      if (!containerRef.current || !maskRef.current || !contentRef.current) return;

      const container = containerRef.current;
      const mask = maskRef.current;
      const content = contentRef.current;

      const handleMouseMove = (e: MouseEvent) => {
        const rect = container.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        gsap.to(mask, {
          '--mask-x': `${x}px`,
          '--mask-y': `${y}px`,
          duration: 0.6,
          ease: 'back.out(1.7)',
        });
      };

      const handleMouseEnter = () => {
        gsap.to(mask, {
          '--mask-size': `${maskSizeLarge}px`,
          duration: 0.4,
          ease: 'power2.out',
        });
      };

      const handleMouseLeave = () => {
        gsap.to(mask, {
          '--mask-size': `${maskSizeSmall}px`,
          duration: 0.3,
          ease: 'power2.in',
        });
      };

      document.addEventListener('mousemove', handleMouseMove);
      content.addEventListener('mouseenter', handleMouseEnter);
      content.addEventListener('mouseleave', handleMouseLeave);

      return () => {
        document.removeEventListener('mousemove', handleMouseMove);
        content.removeEventListener('mouseenter', handleMouseEnter);
        content.removeEventListener('mouseleave', handleMouseLeave);
      };
    },
    { dependencies: [maskSizeSmall, maskSizeLarge] }
  );

  return (
    <div
      ref={containerRef}
      className={cn(
        'relative w-full flex items-center justify-center overflow-hidden cursor-none',
        className
      )}
    >
      <div ref={contentRef} className="relative z-0 flex items-center justify-center w-full">
        {originalContent}
      </div>

      <div
        ref={maskRef}
        className="absolute inset-0 z-10 flex items-center justify-center"
        style={{
          background: maskBackground,
          pointerEvents: 'none',
          maskImage:
            'radial-gradient(circle var(--mask-size, 20px) at var(--mask-x, -50px) var(--mask-y, -50px), black 100%, transparent 100%)',
          WebkitMaskImage:
            'radial-gradient(circle var(--mask-size, 20px) at var(--mask-x, -50px) var(--mask-y, -50px), black 100%, transparent 100%)',
        } as React.CSSProperties}
      >
        {maskContent}
      </div>
    </div>
  );
};

export default MaskRevealOnHover;