Wave Effect
Each letter waves vertically with a progressive CSS delay — JS splits the text into spans on the fly to animate each character independently.
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 IIFE targets document.getElementById('waveText') and replaces its innerHTML with the string "WAVE" split letter by letter: each character becomes a <span> with style="animation-delay: Xs" computed as 0.1 × index seconds. The source text is hardcoded in the script; to use the element's existing content instead, replace "WAVE" with e.textContent.
The CSS rule .text-wave span applies animation: 1s ease-in-out 0s infinite wave and, critically, display: inline-block: without this, transform: translateY() has no effect on inline elements. The keyframe @keyframes wave moves each letter from translateY(0) to translateY(-15px) at mid-duration, then back to zero, creating an infinite sinusoidal vertical oscillation.
The progressive delay creates the wave illusion: letter 0 rises first, letter 1 follows 100 ms later, and so on. With a 4-letter word, the crest travels the full width in 300 ms before the first letter restarts — the wave appears to move continuously from left to right.
The CSS file includes two additional variants: .text-wave-v2 with @keyframes waveV2 (amplitude −10 px, fixed nth-child delays for 6 children) and .text-wave-continuous (2 s duration). The same IIFE also initializes waveTextV2 (text “BOUNCE”), flipText, and a hover scramble on scrambleTextHover — each with an early return if the element is absent from the DOM.
Accessibility
- prefers-reduced-motion absent: no media query or JS check stops the animation when the OS signals this preference. Recommended fix: add
@media (prefers-reduced-motion: reduce) { .text-wave span, .text-wave-v2 span { animation: none; } }. - Child
<span>elements injected by JS carry noaria-hiddenattribute — text content remains intact in the accessibility tree and readable by screen readers. - No keyboard interaction or focus management: the animation is purely decorative/presentational.
- Text/background contrast is not defined by the effect; it depends entirely on the integration context. Verify a WCAG AA ratio ≥ 4.5:1 for normal text.
- No rapid flickering: oscillation frequency is ~1 Hz, well below the 3 Hz threshold for photosensitive seizures (WCAG 2.3.1).
Browser compatibility
Requires only CSS animations (@keyframes, transform) and basic DOM manipulation — universal support in modern browsers.
If JS is disabled, the text stays readable in its container but individual <code><span></code> elements are not created — the animation does not run. No JS errors, no network dependencies.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<span class="demo-text text-wave" id="waveText">
<!-- JS replaces the text with one <span style="animation-delay: Xs"> per letter -->
<span style="animation-delay: 0s">W</span>
<span style="animation-delay: 0.1s">A</span>
<span style="animation-delay: 0.2s">V</span>
<span style="animation-delay: 0.3s">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 |
|---|---|---|
| Wave amplitude (@keyframes wave, 50%) | -15px | translateY value at the midpoint — -6 px for a subtle effect, -30 px for a dramatic bounce. |
| Animation duration (.text-wave span, animation) | 1s | First value of the animation shorthand — 0.6 s for a snappy effect, 2 s for a slow breath. |
| Per-letter delay step (JS, ${.1*t}) | 0.1 | Delay multiplier in the JS expression — 0.05 tightens the wave, 0.2 stretches it over more time. |
| Source text (JS, "WAVE") | "WAVE" | Hardcoded string in e.innerHTML — replace with e.textContent to animate whatever text is already in the HTML. |
| Easing (.text-wave span, animation) | ease-in-out | Timing function — linear for a mechanical feel, cubic-bezier(0.4,0,0.2,1) for a natural spring. |
| Reduced-amplitude variant (CSS class) | .text-wave | Switch to .text-wave-v2 (keyframe waveV2, amplitude -10 px) for subtler motion in compact contexts. |
| Font size (.demo-text, font-size) | 2.5rem | Override per context: 4–6 rem for a hero, 0.85–1 rem for a badge, 2.5–3 rem for an editorial heading. |
FAQ
<span> elements with no aria-hidden attribute — the text stays visible in the DOM and Googlebot reads it as plain text. No cloaking technique, no indexing issues.getElementById('waveText') — one element only. For multiple instances, switch to document.querySelectorAll('[data-wave]') and iterate: for each element, split its textContent into <span> elements with the progressive delay. The logic is identical, just applied in a loop.<span> elements for .text-wave span to target. Two solutions: include the JS (recommended) or manually split the HTML into <span style="animation-delay: Xs"> per letter.