Build a Shiny Text Effect in React
Add a shimmering light sweep animation to text using CSS gradients and keyframes, no JavaScript animation library required.
Some UI elements just need to gleam. A premium label, a "new" badge, a call to action you want people to notice. The shiny text effect does exactly one thing, and it does it well: it drags a highlight across your text like light catching a metallic surface.
The final result
ShinyText applies a moving gradient highlight across a string of text using Motion's useAnimationFrame and useMotionValue. The shimmer sweeps from one side to the other on a continuous loop, with options for yoyo, pause on hover, and direction control.
Setting up
This component uses Motion (the library formerly known as Framer Motion).
import {
motion,
useMotionValue,
useAnimationFrame,
useTransform,
} from 'motion/react';Building the component
The key props are speed, color, shineColor, spread, and direction.
interface ShinyTextProps {
text: string;
disabled?: boolean;
speed?: number;
color?: string;
shineColor?: string;
spread?: number;
yoyo?: boolean;
pauseOnHover?: boolean;
direction?: 'left' | 'right';
delay?: number;
}The gradient is defined as a CSS background image with the shine color sandwiched between the base color. background-clip: text combined with transparent text fill is what makes it show through the letterforms.
const gradientStyle: React.CSSProperties = {
backgroundImage: `linear-gradient(${spread}deg, ${color} 0%, ${color} 35%, ${shineColor} 50%, ${color} 65%, ${color} 100%)`,
backgroundSize: '200% auto',
WebkitBackgroundClip: 'text',
backgroundClip: 'text',
WebkitTextFillColor: 'transparent',
};The animation is driven by useAnimationFrame, which runs every frame and updates a progress motion value. We track elapsed time manually to calculate where in the cycle we are.
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;
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);
}
});The progress value maps to a backgroundPosition using useTransform. Shifting the background position from 150% to -50% moves the shine band from right to left across the text.
const backgroundPosition = useTransform(
progress,
p => `${150 - p * 2}% center`
);How to use it
<ShinyText
text="Premium"
color="#b5b5b5"
shineColor="#ffffff"
speed={2}
spread={120}
direction="left"
pauseOnHover={true}
/>Set yoyo to true if you want the shine to sweep back and forth rather than looping in one direction.
Key takeaways
- The
background-clip: texttrick is the entire foundation of this effect. Without it, the gradient sits behind the text instead of through it. - Using
useAnimationFrameinstead of a CSS animation gives you programmatic control over playback, pause, and direction without CSS class juggling. - The
spreadprop controls the angle of the gradient, which lets you make the shine feel more like a direct light source or more like a diffuse sheen.