How I Created a Hover Reveal Text Animation with React, useRef, and TailwindCSS




When building interactive web interfaces, animations can play a crucial role in enhancing the user experience. One of the animations I recently created was a "hover reveal" effect where the cursor, when hovering over a specific element, reveals text that was previously hidden. Using React, useRef, useEffect, and TailwindCSS, I was able to create a smooth, interactive hover effect.

In this post, I'll walk through the code and explain the thought process behind each step, giving you insights into how to leverage useRef for managing DOM elements and animations. The article will cover:

  1. The overall setup and structure
  2. The useRef and useEffect hooks for managing state and animations
  3. TailwindCSS styling for a responsive and visually appealing look

Let’s dive in!


Overview of the Component

The component consists of three main elements:

  1. Cursor Circle: A dynamic circle that enlarges when the cursor hovers over the target text.
  2. Hover Target: The text that prompts the user to hover.
  3. Clip Path Text: The text hidden behind the main text, which is revealed through a circular "spotlight" effect on hover.

The core logic involves tracking the cursor's position and animating the circle size and the reveal of hidden text using clip-path. To achieve this effect, we use the useRef hook to reference elements directly and control their styles dynamically.


Tech Stack and Tools

  • React: To build the interactive UI component
  • TailwindCSS: For rapid and efficient styling with a utility-first approach
  • JavaScript: Specifically useRef and useEffect for handling state and animations
  • CSS: Applied via both TailwindCSS and custom inline styles to control the clip-path and cursor styles

Building the Component Step-by-Step

Step 1: Setting Up the Refs

First, we initialize several useRef hooks:

  1. hoveredRef: Tracks whether the cursor is over the target element.
  2. clipPathElement: References the hidden text's wrapper for dynamic styling.
  3. animationFrameId: Stores the ID of the current animation frame to manage cancellation.
  4. cursorPosition: Holds the cursor’s x and y coordinates.
  5. cursorCircleRef: Points to the cursor circle that will follow the mouse and change size on hover.

Here’s the initial setup:

import { useRef, useEffect } from "react"; export default function HoverReveal() { const hoveredRef = useRef(null); const clipPathElement = useRef(null); const animationFrameId = useRef(null); const cursorPosition = useRef({ x: 0, y: 0 }); const cursorCircleRef = useRef(null);

Step 2: Hover Effects for Showing and Hiding Text

We define two functions, showHidden and hideText, to handle the behavior of the cursor circle and reveal effects on hover.

  • showHidden: Activates when the cursor enters the hover area, increasing the circle's size and triggering the reveal effect.
  • hideText: Reverts the circle’s size and hides the text when the cursor leaves.
function showHidden() { if (!hoveredRef.current) { hoveredRef.current = true; cursorCircleRef.current.style.width = '300px'; cursorCircleRef.current.style.height = '300px'; } } function hideText() { if (hoveredRef.current) { hoveredRef.current = false; cursorCircleRef.current.style.border = 'none'; cursorCircleRef.current.style.width = '20px'; cursorCircleRef.current.style.height = '20px'; } }

Step 3: Positioning and Animating the Cursor

The setCursor function captures the cursor position, updates the clipPath dynamically, and animates the cursor circle’s position.

  • Cursor Positioning: Captures the x and y values of the mouse and stores them in cursorPosition.
  • Clip Path Animation: When hovered, updates clip-path to reveal the hidden text in a circular shape that follows the cursor.
function setCursor(e) { cursorPosition.current = { x: e.clientX, y: e.clientY }; if (hoveredRef.current) { clipPathElement.current.style.clipPath = `circle(150px at ${cursorPosition.current.x}px ${cursorPosition.current.y + window.scrollY - 320}px)`; } else { clipPathElement.current.style.clipPath = 'circle(0px)'; } if (animationFrameId.current) { cancelAnimationFrame(animationFrameId.current); } animationFrameId.current = requestAnimationFrame(() => { const cursor = cursorCircleRef.current; const targetX = e.clientX; const targetY = e.clientY; const moveCursor = () => { const currentX = cursor.offsetLeft; const currentY = cursor.offsetTop; const dx = targetX - currentX; const dy = targetY - currentY; const speed = 100; const moveX = currentX + dx * speed; const moveY = currentY + dy * speed; cursor.style.left = `${moveX}px`; cursor.style.top = `${moveY}px`; animationFrameId.current = requestAnimationFrame(moveCursor); }; moveCursor(); }); }

Step 4: Setting Up the useEffect Hook

The useEffect hook is crucial for attaching the event listener to mousemove, ensuring the cursor position is tracked across the screen. It also cleans up by removing the event listener and canceling any pending animations on unmount.

useEffect(() => { window.addEventListener('mousemove', setCursor); return () => { window.removeEventListener('mousemove', setCursor); if (animationFrameId.current) { cancelAnimationFrame(animationFrameId.current); } }; }, []);

Step 5: The JSX Structure

The JSX structure leverages ref assignments and TailwindCSS classes to organize the layout. Here’s the complete JSX setup:

return ( <div className="flex justify-center h-screen items-center"> <div ref={cursorCircleRef} style={{transform: 'translate(-50%, -50%)', top: '0', left: '0'}} className="z-[1] fixed pointer-events-none cursorCircle transition-all duration-200 bg-[#6bd490] rounded-full h-5 w-5 stroke-black stroke-2"></div> <div onMouseOver={showHidden} onMouseOut={hideText} className="epilogue-bold text-[84px] text-center leading-none"> <span>Hover me to reveal</span> </div> <div ref={(el) => clipPathElement.current = el} className="z-[2] absolute left-1/2 w-full -translate-x-1/2 flex justify-center" style={{ pointerEvents: 'none', clipPath: 'circle(0px)' }}> <div className="text-[84px] text-center leading-none epilogue-bold text-black"> <span>I am here</span> </div> </div> </div> ); }

TailwindCSS Styling

TailwindCSS allowed us to style the component quickly and concisely:

  • transition-all duration-200 smoothly animates size changes on hover.
  • fixed pointer-events-none ensures the cursor circle is purely visual, not interacting with other elements.
  • z-[1] and z-[2] layers the elements correctly to achieve the reveal effect.

Conclusion

With React and TailwindCSS, creating a hover reveal effect is both streamlined and highly customizable. By leveraging useRef for direct DOM manipulation and useEffect for animation control, this project achieved an interactive, visually appealing effect with only a few lines of code. This approach can be easily adapted for more complex effects and showcases the power of React’s hooks in managing dynamic, real-time animations.

Post a Comment

1 Comments