Building a Text Wave Highlight in React
How the moving text wave in my hero works: split the sentence into characters, track one active index, and shift the highlight across the string.
The animated sentence in my hero is not a transform-based wave. It is a moving highlight wave.
This text:
Experienced and continuously evolving Senior Software Engineer with a passion for innovation and scalable systems. Specialized in full-stack development, AI integration, and blockchain technologies. Adept at leading cross-functional teams and delivering high-quality solutions across diverse domains.
feels alive because one character is highlighted, one character trails behind it, and the rest stay in the base color. Then that highlight moves forward forever.
The idea
The effect is simple:
- Keep one
activeIndexin state. - Advance it on an interval.
- Render every character in its own
<span>. - Change the color of the active character and the trailing character.
That is enough to create a subtle wave or pulse moving through the sentence.
The exact pattern
This is the same structure I use in the portfolio hero:
function ThinkingPulseText({
text,
matrixMode,
}: {
text: string
matrixMode: boolean
}) {
const [activeIndex, setActiveIndex] = useState(0)
useEffect(() => {
const timer = window.setInterval(() => {
setActiveIndex((value) => (value + 1) % text.length)
}, 42)
return () => window.clearInterval(timer)
}, [text.length])
return (
<span>
{text.split("").map((char, index) => {
const isActive = index === activeIndex
const isTrailing = index === (activeIndex + text.length - 1) % text.length
return (
<span
key={`${char}-${index}`}
className={
isActive
? "text-gray-100 dark:text-gray-100"
: isTrailing
? "text-gray-300 dark:text-gray-300"
: "text-gray-600 dark:text-gray-400"
}
style={{ transition: "color 140ms ease" }}
>
{char}
</span>
)
})}
</span>
)
}
Why this works
1. The sentence is split into characters
text.split("") turns one long string into many small pieces.
That lets me style each character independently instead of styling the whole paragraph as one block.
2. One index moves forever
This part advances the highlight:
setActiveIndex((value) => (value + 1) % text.length)
The modulo keeps the number inside the string length, so when the wave reaches the last character it loops back to the start.
3. There is an active character and a trailing character
I use two states per character:
const isActive = index === activeIndex
const isTrailing = index === (activeIndex + text.length - 1) % text.length
The active character is the brightest point of the wave.
The trailing character is slightly dimmer, which makes the movement feel smoother and more deliberate than a single blinking letter.
4. CSS transition softens the movement
Without a transition, the color jump is too sharp.
This line gives the wave a softer handoff:
style={{ transition: "color 140ms ease" }}
The interval is 42ms, but the color transition is 140ms, so the highlights overlap a little. That overlap is what makes it feel like a wave instead of a harsh cursor.
Minimal reusable version
If I wanted to extract this into a small reusable component, I would write it like this:
import { useEffect, useState } from "react"
export function TextWave({
text,
speed = 42,
}: {
text: string
speed?: number
}) {
const [activeIndex, setActiveIndex] = useState(0)
useEffect(() => {
const timer = window.setInterval(() => {
setActiveIndex((value) => (value + 1) % text.length)
}, speed)
return () => window.clearInterval(timer)
}, [speed, text.length])
return (
<span>
{text.split("").map((char, index) => {
const isActive = index === activeIndex
const isTrailing = index === (activeIndex + text.length - 1) % text.length
return (
<span
key={`${char}-${index}`}
className={
isActive
? "text-foreground"
: isTrailing
? "text-foreground/70"
: "text-muted-foreground"
}
style={{ transition: "color 140ms ease" }}
>
{char}
</span>
)
})}
</span>
)
}
Usage:
<TextWave text="This sentence has a moving highlight wave." />
Tuning the feel
There are three levers that matter:
speed: lower numbers move faster.transition: longer transitions make the wave feel softer.contrast: stronger color difference makes the effect more obvious.
Some practical combinations:
- Fast and subtle:
42msinterval,140mstransition. - Slower and calmer:
70msinterval,180mstransition. - More dramatic: add a second trailing character with another lower-opacity color.
When to use this
This works best for:
- hero summaries
- tagline copy
- “thinking” or “processing” states
- text that should feel alive without becoming distracting
I would not use it for large paragraphs of body text everywhere. The effect works because it is selective.
The main takeaway
This animation looks advanced, but the logic is small:
- one moving index
- one trailing index
- one span per character
- a color transition
That is enough to create a convincing text wave.
