Skip to main content
The ShinyText component creates an animated gradient text effect with a moving shine that sweeps across the text.

Overview

ShinyText provides:
  • Animated gradient sweep effect
  • Customizable colors, speed, and direction
  • Yoyo (back-and-forth) animation mode
  • Pause on hover functionality
  • Delay support for staggered animations

Props

text
string
required
The text content to display with the shine effect
disabled
boolean
default:"false"
Disables the animation when true
speed
number
default:"2"
Animation duration in seconds
className
string
default:"''"
Additional CSS classes to apply
color
string
default:"#b5b5b5"
Base text color (gradient start/end)
shineColor
string
default:"#ffffff"
Highlight color for the shine effect
spread
number
default:"120"
Gradient angle in degrees (0-360)
yoyo
boolean
default:"false"
Enables back-and-forth animation
pauseOnHover
boolean
default:"false"
Pauses animation when hovering over text
direction
'left' | 'right'
default:"'left'"
Direction of the shine sweep
delay
number
default:"0"
Delay before animation starts (in seconds)

Implementation

ShinyText.jsx
import { useState, useCallback, useEffect, useRef } from 'react';
import { motion, useMotionValue, useAnimationFrame, useTransform } from 'motion/react';

const ShinyText = ({
  text,
  disabled = false,
  speed = 2,
  className = '',
  color = '#b5b5b5',
  shineColor = '#ffffff',
  spread = 120,
  yoyo = false,
  pauseOnHover = false,
  direction = 'left',
  delay = 0
}) => {
  const [isPaused, setIsPaused] = useState(false);
  const progress = useMotionValue(0);
  const elapsedRef = useRef(0);
  const lastTimeRef = useRef(null);
  const directionRef = useRef(direction === 'left' ? 1 : -1);

  const animationDuration = speed * 1000;
  const delayDuration = delay * 1000;

  useAnimationFrame(time => {
    if (disabled || isPaused) {
      lastTimeRef.current = null;
      return;
    }

    if (lastTimeRef.current === null) {
      lastTimeRef.current = time;
      return;
    }

    const deltaTime = time - lastTimeRef.current;
    lastTimeRef.current = time;
    elapsedRef.current += deltaTime;

    if (yoyo) {
      const cycleDuration = animationDuration + delayDuration;
      const fullCycle = cycleDuration * 2;
      const cycleTime = elapsedRef.current % fullCycle;

      if (cycleTime < animationDuration) {
        const p = (cycleTime / animationDuration) * 100;
        progress.set(directionRef.current === 1 ? p : 100 - p);
      } else if (cycleTime < cycleDuration) {
        progress.set(directionRef.current === 1 ? 100 : 0);
      } else if (cycleTime < cycleDuration + animationDuration) {
        const reverseTime = cycleTime - cycleDuration;
        const p = 100 - (reverseTime / animationDuration) * 100;
        progress.set(directionRef.current === 1 ? p : 100 - p);
      } else {
        progress.set(directionRef.current === 1 ? 0 : 100);
      }
    } else {
      const cycleDuration = animationDuration + delayDuration;
      const cycleTime = elapsedRef.current % cycleDuration;

      if (cycleTime < animationDuration) {
        const p = (cycleTime / animationDuration) * 100;
        progress.set(directionRef.current === 1 ? p : 100 - p);
      } else {
        progress.set(directionRef.current === 1 ? 100 : 0);
      }
    }
  });

  const backgroundPosition = useTransform(
    progress, 
    p => `${150 - p * 2}% center`
  );

  const gradientStyle = {
    backgroundImage: `linear-gradient(${spread}deg, ${color} 0%, ${color} 35%, ${shineColor} 50%, ${color} 65%, ${color} 100%)`,
    backgroundSize: '200% auto',
    WebkitBackgroundClip: 'text',
    backgroundClip: 'text',
    WebkitTextFillColor: 'transparent'
  };

  return (
    <motion.span
      className={`inline-block ${className}`}
      style={{ ...gradientStyle, backgroundPosition }}
      onMouseEnter={() => pauseOnHover && setIsPaused(true)}
      onMouseLeave={() => pauseOnHover && setIsPaused(false)}
    >
      {text}
    </motion.span>
  );
};

export default ShinyText;

Usage Examples

Basic Usage

import ShinyText from './components/ShinyText/ShinyText';

function Hero() {
  return (
    <h1>
      <ShinyText text="MUSIC STORE" />
    </h1>
  );
}

Custom Colors and Speed

<ShinyText
  text="ENCUENTRA TU SONIDO IDEAL"
  speed={4}
  color="#c9a84c"
  shineColor="#ffe566"
  spread={120}
  direction="left"
/>

Yoyo Animation

<ShinyText
  text="Featured Product"
  yoyo={true}
  speed={3}
  pauseOnHover={true}
/>

With Delay (Staggered)

<div>
  <ShinyText text="LINE 1" delay={0} />
  <ShinyText text="LINE 2" delay={0.5} />
  <ShinyText text="LINE 3" delay={1.0} />
</div>

Animation Behavior

Standard Mode

The shine sweeps from one side to the other, then resets:
  1. Shine enters from off-screen
  2. Moves across the text
  3. Exits off-screen
  4. Delay period (if specified)
  5. Repeats

Yoyo Mode

The shine sweeps back and forth:
  1. Forward sweep
  2. Delay at end
  3. Reverse sweep
  4. Delay at start
  5. Repeats

Gradient Mechanics

The component uses CSS gradient background with text clipping:
background-image: linear-gradient(
  {spread}deg, 
  {color} 0%, 
  {color} 35%, 
  {shineColor} 50%, 
  {color} 65%, 
  {color} 100%
);
background-size: 200% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
The background position animates from 150% to -50% to create the sweep effect.

Real-World Example

From Hero component:
Hero.jsx
<ShinyText
  text={"ENCUENTRA TU SONIDO\nIDEAL CON NOSOTROS"}
  speed={4}
  delay={0}
  color="#c9a84c"
  shineColor="#ffe566"
  spread={120}
  direction="left"
  yoyo={false}
  pauseOnHover={false}
  disabled={false}
  className="whitespace-pre text-sm md:text-base font-bold"
  style={{ 
    fontFamily: "'Bebas Neue', sans-serif", 
    letterSpacing: "0.10em" 
  }}
/>
Use Framer Motion’s useAnimationFrame instead of requestAnimationFrame for better integration with React’s rendering cycle.
The component requires the motion library from Framer Motion for smooth animation performance.