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.

Build docs developers (and LLMs) love