Wave Text — Free HTML CSS JS Animation Snippet
Wave Text Animation · Animations · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="scene">
<div class="wave-wrap" id="wave1"></div>
<div class="wave-wrap rainbow" id="wave2"></div>
<div class="controls">
<button class="btn" onclick="setWord('WAVE EFFECT')">WAVE EFFECT</button>
<button class="btn" onclick="setWord('HELLO WORLD')">HELLO WORLD</button>
<button class="btn" onclick="setWord('LOADING...')">LOADING...</button>
</div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #050810; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.scene { display: flex; flex-direction: column; align-items: center; gap: 32px; padding: 24px; }
.wave-wrap { display: flex; gap: 2px; height: 60px; align-items: center; }
.letter {
font-size: 36px; font-weight: 900;
font-family: 'Courier New', monospace;
color: #6366f1;
display: inline-block;
animation: wave 1.2s ease-in-out infinite;
text-shadow: 0 0 20px rgba(99,102,241,0.5);
min-width: 0.6em; text-align: center;
}
.rainbow .letter { color: transparent; }
.rainbow .letter:nth-child(1) { color: #ef4444; text-shadow: 0 0 20px rgba(239,68,68,0.5); }
.rainbow .letter:nth-child(2) { color: #f97316; text-shadow: 0 0 20px rgba(249,115,22,0.5); }
.rainbow .letter:nth-child(3) { color: #f59e0b; text-shadow: 0 0 20px rgba(245,158,11,0.5); }
.rainbow .letter:nth-child(4) { color: #22c55e; text-shadow: 0 0 20px rgba(34,197,94,0.5); }
.rainbow .letter:nth-child(5) { color: #0ea5e9; text-shadow: 0 0 20px rgba(14,165,233,0.5); }
.rainbow .letter:nth-child(6) { color: #6366f1; text-shadow: 0 0 20px rgba(99,102,241,0.5); }
.rainbow .letter:nth-child(7) { color: #8b5cf6; text-shadow: 0 0 20px rgba(139,92,246,0.5); }
.rainbow .letter:nth-child(8) { color: #ec4899; text-shadow: 0 0 20px rgba(236,72,153,0.5); }
.rainbow .letter:nth-child(9) { color: #ef4444; text-shadow: 0 0 20px rgba(239,68,68,0.5); }
.rainbow .letter:nth-child(10) { color: #f97316; text-shadow: 0 0 20px rgba(249,115,22,0.5); }
.rainbow .letter:nth-child(11) { color: #22c55e; }
.rainbow .letter:nth-child(12) { color: #0ea5e9; }
@keyframes wave {
0%,100% { transform: translateY(0); }
50% { transform: translateY(-16px); }
}
.controls { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.btn { padding: 7px 14px; font-size: 11px; font-weight: 700; letter-spacing: 1px; font-family: 'Courier New', monospace; background: transparent; color: #334155; border: 1px solid #1e293b; border-radius: 4px; cursor: pointer; transition: all 0.15s; }
.btn:hover { border-color: #6366f1; color: #6366f1; }function buildWave(id, text) {
const el = document.getElementById(id);
el.innerHTML = '';
text.split('').forEach((ch, i) => {
const span = document.createElement('span');
span.className = 'letter';
span.textContent = ch === ' ' ? ' ' : ch;
span.style.animationDelay = (i * 0.08) + 's';
el.appendChild(span);
});
}
function setWord(w) {
buildWave('wave1', w);
buildWave('wave2', w);
}
setWord('WAVE EFFECT');Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Wave Text — buildWave() Character Wrapping, animation-delay Stagger

Wave text animates each character in a heading individually — characters rise and fall in a sequential wave pattern. Used on interactive headings, loading messages, and any text that benefits from playful motion — see also split text reveals, a typewriter effect, and gradient text.
The character-splitting technique
buildWave(id, text) splits the text string into individual characters via .split(''). Each character is wrapped in a <span> with a unique animation-delay: i * 0.05s — the delay increases by 50ms per character, creating the left-to-right wave timing. Spaces are replaced with non-breaking spaces ( ) to preserve spacing.
The CSS wave animation
Each span has a CSS @keyframes wave animation that runs translateY(-12px) at 50% and returns to translateY(0) at 100%. With staggered delays, the upward peak travels from character 0 to the last character, creating the wave.
Hover re-trigger
Hovering the wave element triggers the animation again by briefly removing and re-adding the animation class — creating an on-demand wave that plays each time the user hovers.
The staggered animation-delay technique
Wave text works by applying the same keyframe animation to every character span, but with a progressively increasing animation-delay. Character 0 starts immediately; character 1 waits 0.05s; character N waits N×0.05s. The keyframe itself is identical — translateY(-12px) at 50% and translateY(0) at 0%/100%. The delay offset is all that creates the wave appearance.
Character splitting with JavaScript
The text content is split into individual characters, each wrapped in a span: [...text].forEach((char, i) => { const span = document.createElement('span'); span.textContent = char === ' ' ? ' ' : char; span.style.animationDelay = i * 0.05 + 's'; el.appendChild(span); }). The non-breaking space ( ) replaces regular spaces so they preserve their width in the inline span layout.
Controlling the wave properties
Change the delay multiplier (0.05s) to control wave speed — smaller values create a faster wave, larger values create a slower, more dramatic roll. Change the translateY value in the keyframe to control wave height. Add translateX for a sideways sway. Use a sine-based delay function for a smoother wave curve: Math.sin(i * 0.4) * 0.1 + 'delay'.
Accessibility and prefers-reduced-motion
The animation can be distracting for users sensitive to motion. Add @media (prefers-reduced-motion: reduce) { .wave-char { animation: none; } } to freeze all characters for users who have enabled the reduce motion accessibility setting. The text remains fully readable — only the decorative motion is removed.
The staggered animation-delay technique
Wave text works by applying the same keyframe animation to every character span, but with a progressively increasing animation-delay. Character 0 starts immediately; character 1 waits 0.05s; character N waits N×0.05s. The keyframe itself is identical — translateY(-12px) at 50% and translateY(0) at 0%/100%. The delay offset is all that creates the wave appearance.
Character splitting with JavaScript
The text content is split into individual characters, each wrapped in a span: [...text].forEach((char, i) => { const span = document.createElement('span'); span.textContent = char === ' ' ? ' ' : char; span.style.animationDelay = i * 0.05 + 's'; el.appendChild(span); }). The non-breaking space ( ) replaces regular spaces so they preserve their width in the inline span layout.
Controlling the wave properties
Change the delay multiplier (0.05s) to control wave speed — smaller values create a faster wave, larger values create a slower, more dramatic roll. Change the translateY value in the keyframe to control wave height. Add translateX for a sideways sway. Use a sine-based delay function for a smoother wave curve: Math.sin(i * 0.4) * 0.1 + 'delay'.
Accessibility and prefers-reduced-motion
The animation can be distracting for users sensitive to motion. Add @media (prefers-reduced-motion: reduce) { .wave-char { animation: none; } } to freeze all characters for users who have enabled the reduce motion accessibility setting. The text remains fully readable — only the decorative motion is removed.
Step by step
How to Use
- 1Watch the wave animateThe wave cycles through the text continuously. Characters rise in sequence from left to right.
- 2Hover to replayMove the cursor over the wave text to trigger the animation on demand.
- 3Update the textIn the JS panel, update the text arguments in the buildWave() calls to your own phrases.
- 4Change animation delayUpdate i * 0.05 in the JS animation-delay calculation. Smaller values (0.03) create faster waves; larger (0.08) create slower ones.
- 5Change wave heightUpdate the translateY(-12px) value in the CSS wave keyframe.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
buildWave() assigns animation-delay: i * 0.05s to each span, where i is the character index. Character 0 starts at 0s, character 1 at 0.05s, character 2 at 0.1s. The translateY peak travels left to right as each character reaches its 50% keyframe at a slightly later time.
When text is split into individual characters, regular spaces between words are rendered as empty spans with zero width. Non-breaking space (\u00A0) has a visible width, preserving the gap between words.
Replace translateY(-12px) with translateX(12px) for a horizontal wave, or use a combination: transform: translateY(-12px) rotate(10deg) for a rotation wave.
Wrap the wave trigger in an IntersectionObserver. When the element enters the viewport, call buildWave() (which re-creates all spans and restarts animations from delay 0).
Yes. Click "JSX" for a React component. Split the text string into characters, map each to a span with style={{ animationDelay: i * 0.05 + "s" }}. Increment a key on hover to re-trigger by remounting the spans.
Add a colour array and assign span.style.color = colors[i % colors.length] in buildWave(). Combine with the delay stagger for a rainbow wave effect.