Docs
Text Hover Effect

Text Hover Effect

Highligts the text on hover

unizoyunizoyunizoy

Install the following dependencies:

pnpm add gsap react-icons

Make a file and copy paste this code in a file with name text-hover-effect.tsx

"use client"
import React, { useRef, useState, useEffect } from "react"
import { useGSAP } from "@gsap/react"
import gsap from "gsap"
 
export const TextHoverEffect = ({
  text,
  duration,
  fontSize = 56, // Default to roughly Tailwind's `text-7xl`
}: {
  text: string
  duration?: number
  fontSize?: number
}) => {
  const svgRef = useRef<SVGSVGElement>(null)
  const maskGradientRef = useRef(null)
  const animatedTextRef = useRef(null)
  const [cursor, setCursor] = useState({ x: 0, y: 0 })
  const [hovered, setHovered] = useState(false)
  const [maskPosition, setMaskPosition] = useState({ cx: "50%", cy: "50%" })
 
  useGSAP(
    () => {
      gsap.fromTo(
        animatedTextRef.current,
        { strokeDashoffset: 1000, strokeDasharray: 1000 },
        {
          strokeDashoffset: 0,
          strokeDasharray: 1000,
          duration: 4,
          ease: "power2.inOut",
        }
      )
    },
    { scope: svgRef }
  )
 
  // Update cursor position based on mouse or touch coordinates
  const updateCursorPosition = (x: number, y: number) => {
    if (svgRef.current && x !== null && y !== null) {
      const svgRect = svgRef.current.getBoundingClientRect()
      const cxPercentage = ((x - svgRect.left) / svgRect.width) * 100
      const cyPercentage = ((y - svgRect.top) / svgRect.height) * 100
 
      const newPosition = {
        cx: `${cxPercentage}%`,
        cy: `${cyPercentage}%`,
      }
 
      setMaskPosition(newPosition)
 
      gsap.to(maskGradientRef.current, {
        attr: newPosition,
        duration: duration ?? 0,
        ease: "power2.out",
      })
    }
  }
 
  useEffect(() => {
    updateCursorPosition(cursor.x, cursor.y)
  }, [cursor, duration])
 
  // Mouse event handlers
  const handleMouseEnter = () => setHovered(true)
  const handleMouseLeave = () => setHovered(false)
  const handleMouseMove = (e: React.MouseEvent) => {
    setCursor({ x: e.clientX, y: e.clientY })
  }
 
  // Touch event handlers
  const handleTouchStart = (e: React.TouchEvent) => {
    e.preventDefault() // Prevent default behavior like scrolling
    setHovered(true)
    if (e.touches.length > 0) {
      const touch = e.touches[0]
      setCursor({ x: touch.clientX, y: touch.clientY })
    }
  }
 
  const handleTouchMove = (e: React.TouchEvent) => {
    e.preventDefault()
    if (e.touches.length > 0) {
      const touch = e.touches[0]
      setCursor({ x: touch.clientX, y: touch.clientY })
    }
  }
 
  const handleTouchEnd = (e: React.TouchEvent) => {
    e.preventDefault()
    setHovered(false)
  }
 
  return (
    <svg
      ref={svgRef}
      width="100%"
      height="100%"
      viewBox="0 0 300 100"
      xmlns="http://www.w3.org/2000/svg"
      // Mouse events
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      onMouseMove={handleMouseMove}
      // Touch events
      onTouchStart={handleTouchStart}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleTouchEnd}
      onTouchCancel={handleTouchEnd}
      className="select-none"
    >
      <defs>
        <linearGradient
          id="textGradient"
          gradientUnits="userSpaceOnUse"
          cx="50%"
          cy="50%"
          r="20%"
        >
          {hovered && (
            <>
              <stop offset="0%" stopColor="#eab308" />
              <stop offset="25%" stopColor="#ef4444" />
              <stop offset="50%" stopColor="#3b82f6" />
              <stop offset="75%" stopColor="#06b6d4" />
              <stop offset="100%" stopColor="#8b5cf6" />
            </>
          )}
        </linearGradient>
        <radialGradient
          id="revealMask"
          ref={maskGradientRef}
          gradientUnits="userSpaceOnUse"
          r="25%"
          cx={maskPosition.cx}
          cy={maskPosition.cy}
        >
          <stop offset="0%" stopColor="white" />
          <stop offset="100%" stopColor="black" />
        </radialGradient>
        <mask id="textMask">
          <rect
            x="0"
            y="0"
            width="100%"
            height="100%"
            fill="url(#revealMask)"
          />
        </mask>
      </defs>
 
      {/* 3 text layers with shared style */}
      {[0, 1, 2].map((_, idx) => (
        <text
          key={idx}
          ref={idx === 1 ? animatedTextRef : undefined}
          x="50%"
          y="50%"
          textAnchor="middle"
          dominantBaseline="middle"
          strokeWidth="0.3"
          className={`fill-transparent font-[helvetica] font-bold ${
            idx === 0
              ? "stroke-neutral-200 dark:stroke-neutral-800"
              : idx === 1
                ? "stroke-neutral-200 dark:stroke-neutral-800"
                : ""
          }`}
          stroke={idx === 2 ? "url(#textGradient)" : undefined}
          mask={idx === 2 ? "url(#textMask)" : undefined}
          style={{
            fontSize,
            opacity: idx === 0 && !hovered ? 0 : idx === 0 ? 0.7 : 1,
          }}
        >
          {text}
        </text>
      ))}
    </svg>
  )
}

Props

NameTypeDescription
text*stringText that has to have the animation
durationnumberTime in seconds to reach the final point of animation.
fontSizenumberFont size in pixels default is 56 same as text-7xl.

Note: Props marked with * are required.