Wave Animation V2
Letters in a wave with a 100 ms per-rank stagger: @keyframes waveV2 pure CSS, JS injects delays for any text length, zero dependencies.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Text has 56 effects, including 4 free. Explore the category →
3 usage examples



How it works
The animation relies on @keyframes waveV2: a simple translateY(0) → translateY(−10 px) → translateY(0) over 1 s, ease-in-out, looping infinitely. Each letter is a <span> with display: inline-block — required for transform to apply on inline elements.
The wave emerges from progressive stagger between letters: the JS computes animation-delay: 0.1s × index and injects it as a style attribute on each <span> generated via "BOUNCE".split("").map((ch, i) => ...). The CSS also declares delays via :nth-child(1..6) — a fallback when JS is absent or spans are pre-built in HTML.
The JS IIFE shares its scope with other effects on the same page (#waveText, #flipText, #scrambleTextHover) using sequential guards: if any getElementById returns null, the function exits immediately. For isolated use, pre-built spans in HTML with the correct inline animation-delay values are sufficient — the animation runs 100% in CSS, no JS needed.
To change the animated text, update the "BOUNCE" string in the JS, or pre-build your <span> elements directly in HTML with style="animation-delay: Ns". Beyond 6 letters, only the JS applies correct delays to additional characters — the :nth-child CSS rules cover only the first 6.
Accessibility
- ⚠️ prefers-reduced-motion not checked in the code: the animation runs unconditionally. For production, add
@media (prefers-reduced-motion: reduce) { .text-wave-v2 span { animation: none } }. - No
aria-labelon the container: screen readers may read letter by letter. Addaria-label="YOUR TEXT"on the parent<span>andaria-hidden="true"on each letter child. - Excellent contrast: white text (
#fff) on dark background (#0a0a0f), ratio > 18:1, WCAG AAA — holds for any light color on a dark ground. - No keyboard interaction or focus management — purely decorative effect, no focus handling required.
- Without CSS, the text remains visible as a flat word: readability is preserved.
Browser compatibility
CSS animation + transform: translateY and display: inline-block — supported in all modern browsers. The JS IIFE is plain vanilla (getElementById, innerHTML, split).
If JS is absent or errors out, pre-built <code><span></code> elements in HTML keep their inline <code>animation-delay</code> and continue animating via CSS. If CSS is disabled, text displays flat and remains readable.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<span class="demo-text text-wave-v2" id="waveTextV2"
aria-label="BOUNCE">
<!-- 1 <span> per letter — 0.1 s progressive stagger -->
<span aria-hidden="true" style="animation-delay: 0s">B</span>
<span aria-hidden="true" style="animation-delay: 0.1s">O</span>
<span aria-hidden="true" style="animation-delay: 0.2s">U</span>
<span aria-hidden="true" style="animation-delay: 0.3s">N</span>
<span aria-hidden="true" style="animation-delay: 0.4s">C</span>
<span aria-hidden="true" style="animation-delay: 0.5s">E</span>
</span>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| translateY(−10px) in @keyframes waveV2 | −10 px | Vertical amplitude: raise to −20 px for a bold wave, drop to −5 px for a subtle shimmer. |
| animation: 1s ease-in-out … on .text-wave-v2 span | 1 s | Duration of one full cycle. 0.6 s = quick nervous wave, 2 s = slow and fluid motion. |
| ease-in-out in animation (timing-function) | ease-in-out | Speed curve per letter. Try cubic-bezier(.4,0,.6,1) for a more pronounced bounce. |
| 0.1 in the JS (${.1*t}s) | 0.1 s | Per-letter stagger delay. 0.05 s = tight fast wave, 0.2 s = wide spread across a long word. |
| font-size: 2.5rem on .demo-text | 2.5 rem | Text size — the wave is most impressive at large sizes (4 rem+). Override freely via CSS on the parent. |
| font-weight: 800 on .demo-text | 800 | Weight — drop to 400–600 for a light editorial title, keep 700–900 for a strong graphic impact. |
| letter-spacing: −0.02em on .demo-text | −0.02 em | Letter spacing. Set to 0.08–0.15 em on short uppercase text to open up the wave. |
| color on .demo-text or its parent | inherited (#fff) | Text color — no dedicated CSS variable; override via color CSS on the element or a parent. |
FAQ
split("") rebuilds spans for any length. Beyond 6 letters, the CSS :nth-child rules cover only the first 6 — letters beyond that have no CSS delay. If you pre-build spans without JS, add inline animation-delay manually for each extra letter.infinite with 1 in animation-iteration-count on .text-wave-v2 span. To trigger the wave on scroll, remove the class text-wave-v2 on load and add it back via an IntersectionObserver when the element enters the viewport — the wave plays once then stops.<span> elements in HTML with inline animation-delay keep animating in pure CSS even if JS is absent. Only the dynamic text rebuild (innerHTML) is lost. For a 100% CSS approach, pre-build your spans manually — sufficient for short, static text.