Wave Text
Pure CSS animation: each letter bounces vertically with a staggered 0.08 s delay — a continuous wave with zero JavaScript.
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 is driven by @keyframes ktWave: from translateY(0) to translateY(-18px) and back, over 2 s with an ease-in-out infinite loop. Each <span class="char"> carries the CSS custom property --delay — 0 s for the first character, +0.08 s per subsequent one — assigned to animation-delay via .kt-wave .char { animation-delay: var(--delay) }. This offset produces the left-to-right wave propagation.
Each .char is display: inline-block — essential because translateY has no effect on inline elements. The user-select: none property on .kt-text prevents accidental selection of the animated text. No canvas or JS renderer is involved: the browser handles the animation entirely on the compositor layer.
Splitting text into spans is either pre-generated in the HTML (the static approach used in the supplied code) or produced dynamically by splitChars(el), which reads data-text or the element's textContent. Note: splitChars does not set --delay values — those must be added via CSS or a supplementary script after the call.
The CSS file bundles several ready-to-use variants beyond .kt-wave: .kt-elastic (one-shot entrance with elastic squash-and-stretch), .kt-scatter / .kt-whirlpool (assembly from scattered positions), .kt-magnetic (cursor repulsion), .kt-liquid (per-letter hover deformation), and .kt-explosion (click to explode). The active variant is selected by the second class on .kt-text — no keyframe changes needed.
Accessibility
- prefers-reduced-motion absent: the code has no
@media (prefers-reduced-motion: reduce)rule — the infinite animation runs even when the OS requests motion reduction. Recommendation: add@media (prefers-reduced-motion: reduce) { .kt-wave .char { animation: none; } }. - Individual
.charelements carry noaria-hidden— screen readers spell out each letter separately (incoherent output). Recommendation: addrole="img" aria-label="[full text]"on the.kt-textcontainer andaria-hidden="true"on each.char. - White text (#fff) on dark background (#0a0a0f) exceeds an 18:1 contrast ratio — meets WCAG AA and AAA.
user-select: noneis set on.kt-text: text cannot be selected or copied. Acceptable for decorative text; remove it if the text conveys information.- The
.kt-waveanimation requires no keyboard or mouse interaction — no focus trap is introduced, keyboard navigation passes freely through the component.
Browser compatibility
Relies on CSS animations and CSS custom properties (var(--delay)) — stable in all modern browsers since 2017.
If CSS custom properties are not supported, all characters animate in sync (simultaneous bounce). Text stays readable and no JS error is thrown.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="demo-preview">
<div class="kt-text kt-wave" id="wave1" data-text="WAVE EFFECT"
role="img" aria-label="Wave Effect">
<!-- One <span class="char"> per character, --delay incremented by 0.08 s -->
<span class="char" data-char="W" style="--delay: 0s;" aria-hidden="true">W</span>
<span class="char" data-char="A" style="--delay: 0.08s;" aria-hidden="true">A</span>
<span class="char" data-char="V" style="--delay: 0.16s;" aria-hidden="true">V</span>
<span class="char" data-char="E" style="--delay: 0.24s;" aria-hidden="true">E</span>
<!-- … -->
</div>
</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 |
|---|---|---|
| animation-duration (.kt-wave .char) | 2s | Duration of one wave cycle. Drop to 1.2 s for a lively wave; raise to 3 s for slow, fluid motion. |
| translateY in @keyframes ktWave (50%) | -18px | Vertical bounce amplitude. Increase to -30 px for a pronounced wave, reduce to -8 px for a subtle effect. |
| --delay (per .char) | 0.08 s per letter | Timing offset between letters. A larger gap (0.15 s) stretches the wave; a smaller gap (0.04 s) tightens it. |
| animation-timing-function (.kt-wave .char) | ease-in-out | Motion curve. cubic-bezier(.45, 0, .55, 1) gives a more elastic wave; linear produces a steady mechanical roll. |
| font-size (.kt-text) | 2.2rem | Text size. Use 4–5 rem for a hero, 1.6 rem for a badge or label. |
| color (.kt-text) | #fff | Letter color. Combine background: linear-gradient(…) + -webkit-background-clip: text + color: transparent for gradient text. |
| letter-spacing (.kt-text) | 0.02em | Inter-letter spacing. Increase to 0.15 em for a poster look; reduce to -0.02 em for compact text. |
| Active variant (.kt-text) | .kt-wave | Replace .kt-wave with .kt-elastic, .kt-liquid, or .kt-explosion to switch animation without touching keyframes. |
FAQ
.kt-wave animation is 100% CSS. If the HTML is pre-built with <span class="char"> elements and inline --delay values, zero JS is needed. The splitChars() function is an optional helper to generate those spans dynamically; it does not set delays (add them via CSS or a supplementary script).translateY value in @keyframes ktWave at 50% — currently -18px. Increase the absolute value for a higher wave, reduce it for a subtler effect. You can also make amplitude vary per letter by substituting a CSS custom property --amp for the fixed value in the keyframe.visibilitychange listener or IntersectionObserver is needed to save CPU.