Wave Animation
Each letter waves up and down in an infinite loop using staggered CSS delays — pure CSS, zero JS for the animation.
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 wave relies entirely on a @keyframes waveContinuous with three steps: 0% and 100% at translateY(0), and 50% at translateY(-15px). The ease-in-out curve gives both the rise and fall a natural, sine-like rhythm.
The animation is assigned to every child <span> via the rule .text-wave-continuous span { animation: 2s ease-in-out infinite waveContinuous }. Phase offset is injected as inline style (animation-delay: 0s, 0.1s, 0.2s…): each letter enters the wave 100 ms after its neighbour.
At load time, the HTML already contains the pre-built spans with their delays. The splitText(el, text, step) utility — referenced from within the JS IIFE — can rebuild those spans on-the-fly from a string: it loops over each character, creates a <span style="animation-delay:…">, and inserts it into the target element.
The Replay button empties the element (innerHTML = '') then calls splitText after 50 ms. This delay is needed to force the browser to reset the CSS timers — without it, animations restart immediately without honouring their animation-delay.
Accessibility
- No prefers-reduced-motion: the stylesheet contains no
@media (prefers-reduced-motion: reduce)block. On systems set to reduced motion, the animation keeps running. To disable it:@media (prefers-reduced-motion:reduce){.text-wave-continuous span{animation:none}}. - The full text remains readable by screen readers through the textual content of each
<span>— noaria-hiddenis set in the provided source. To prevent letter-by-letter reading, addaria-label="YOUR TEXT"on the parent andaria-hidden="true"on each child span. - Contrast: white text (
#fff) on background#0a0a0f→ ratio ≥ 18:1, WCAG AAA. - No keyboard interaction is triggered by the effect itself. The Replay button is a native
<button>, inherently focusable and keyboard-activatable. - The effect is 100% CSS: no
requestAnimationFrameloop, no continuous JS computation — minimal CPU impact on battery-powered devices.
Browser compatibility
Relies on CSS @keyframes, transform: translateY(), and animation-delay — natively supported for years across all modern browsers.
Without <code>@keyframes</code> support (very old browsers), the text displays statically at its rest position — no JS errors, content remains readable.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="demo-preview">
<span class="demo-text text-wave-continuous"
id="waveContinuousText"
aria-label="YOUR TEXT">
<span aria-hidden="true">W</span>
<span aria-hidden="true">A</span>
<span aria-hidden="true">V</span>
<span aria-hidden="true">E</span>
</span>
<button class="replay-btn" onclick="replayWave()">Replay</button>
</div>
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(-15px) | -15px | Vertical wave amplitude in @keyframes waveContinuous (the 50% step). Increase for a more pronounced wave, reduce for a subtle effect. |
| 2s (animation duration) | 2s | Full cycle duration in .text-wave-continuous span { animation: 2s … }. Reduce to 1 s for a fast rhythm, increase to 4 s for slow, serene motion. |
| ease-in-out (timing) | ease-in-out | Animation timing curve. ease-in-out = natural motion; linear = mechanical oscillation; cubic-bezier(0.45,0,0.55,1) accentuates the extremes. |
| animation-delay step (0.1s) | 0.1s | Delay gap between each letter (3rd argument of splitText or inline value). Reduce to 0.06 s for long words; increase to 0.18 s for slower, more dramatic ripple. |
| font-size: 2.5rem (.demo-text) | 2.5rem | Text size. The effect is most striking at large sizes (4–8 rem). Adjust on .demo-text or your own wrapper class. |
| letter-spacing: -0.02em (.demo-text) | -0.02em | Letter spacing. Tight by default for a modern look; set to 0.05–0.15 em for an airier wave where each letter has more breathing room. |
FAQ
@keyframes waveContinuous and animation-delay handle everything. JavaScript only handles span reconstruction (the splitText utility) and the Replay button — removing the JS leaves the animation working as long as the spans are already in the HTML.<span id="waveContinuousText"> with your own <span style="animation-delay: Xs">Letter</span>, incrementing X by 0.1 s per letter. Or call splitText(el, 'YOUR TEXT', 0.1) in JS to build the spans dynamically.animation-delay timers count from when the element is inserted in the DOM. Simply replacing the spans without removing them first means the browser continues the existing timers and the animation does not restart from zero. Clearing the element + a 50 ms pause forces the browser to reset all timers.