Wave Animation
Each letter oscillates between font-weight 300 and 900 via a CSS keyframe, a 0.15 s per-character delay propagating a weight wave from left to right.
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 a single @keyframes vfWave CSS rule controlling three properties at once: font-weight moves from 300 (light) to 900 (black) at the peak, transform: translateY lifts the letter by 10 px at maximum weight, and color shifts from white at 60% opacity to violet #8b5cf6. Synchronising these three axes on a single animation gives the oscillation its sculptural quality.
Each letter is an independent <span class="vf-wave-char"> whose start offset is injected as an inline style (animation-delay: N × 0.15 s). This progressive offset is what creates the wave: all letters run the same 2 s cycle but start at staggered moments. An 8-letter word produces a total phase window of 1.05 s.
font-weight interpolation between 300 and 900 is smooth only with a variable font supporting the wght axis (e.g. Inter Variable, Roboto Flex, Plus Jakarta Sans). With a static font the browser jumps between available weights (400, 700…); the effect remains readable but less fluid. The declared family is Inter, sans-serif — without explicitly loading Inter Variable, the result depends on the local installation.
The JS field is empty: the effect is 100% CSS. There is no requestAnimationFrame, no IntersectionObserver, no public API — the animation starts as soon as the classes are in the DOM and loops indefinitely. To pause it, apply animation-play-state: paused to .vf-wave-char via JS, or remove the class.
Accessibility
- prefers-reduced-motion not implemented: no
@media (prefers-reduced-motion: reduce)rule is defined — the animation loops indefinitely regardless of OS preference. Add manually:@media (prefers-reduced-motion: reduce) { .vf-wave-char { animation: none; font-weight: 400; color: #fff; } } - The text is native HTML content (spans in the normal flow) — screen readers read the letters in order; no canvas or aria-label required.
- The resting color
rgba(255,255,255,0.6)on#0a0a0fmeets WCAG AA for large text (≥ 18 pt); the peak color#8b5cf6reaches a ratio ≈ 4.6:1 on a black background, passing AA for large text. - No interactive element in the default structure — no focus or keyboard handling required.
- No
roleoraria-labelis set by default. If the animated text is a page title, wrap.vf-wave-textin an<h1>or<h2>to stay within the semantic heading hierarchy.
Browser compatibility
Requires CSS @keyframes and animation — supported in all modern browsers for years. Smooth font-weight interpolation requires a variable font (axis wght); without it, weights snap to available values.
If the font is not variable, <code>font-weight</code> jumps between available weights instead of interpolating — the color effect and <code>translateY</code> remain active. No JS errors; the page stays functional.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="vf-wave-scene">
<div class="vf-wave-text" id="vfWaveText">
<span class="vf-wave-char" style="animation-delay:0s;">V</span>
<span class="vf-wave-char" style="animation-delay:0.15s;">O</span>
<span class="vf-wave-char" style="animation-delay:0.30s;">T</span>
<span class="vf-wave-char" style="animation-delay:0.45s;">R</span>
<span class="vf-wave-char" style="animation-delay:0.60s;">E</span>
<!-- one .vf-wave-char per letter — animation-delay +0.15 s per character -->
</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-delay (HTML inline style) | 0.15 s × index | Start offset per letter. Reduce to 0.08 s for a fast wave, increase to 0.25 s for a slow dramatic sweep. |
| animation-duration (.vf-wave-char) | 2s | Full cycle duration per letter. 1 s = dynamic, 4 s = slow and cinematic. |
| font-weight 0%/100% (@keyframes vfWave) | 300 | Resting weight. 100 = ultralight, 400 = regular. Lower values increase contrast with the peak. |
| font-weight 50% (@keyframes vfWave) | 900 | Peak weight. 700 = subtle bold, 900 = maximum black. Only interpolates smoothly with a variable font. |
| translateY 50% (@keyframes vfWave) | -10px | Vertical lift at peak weight. 0px = pure weight change with no movement, 20px = noticeable bounce. |
| color 50% (@keyframes vfWave) | #8b5cf6 | Accent colour at full weight. Swap for your brand primary colour. |
| color 0%/100% (@keyframes vfWave) | rgba(255,255,255,.6) | Colour and opacity at rest. rgba(255,255,255,1) for always-fully-visible letters. |
| font-size (.vf-wave-char) | 2.5rem | Character size. 1.5 rem for a card title, up to 8 rem for a full-width hero. The variable font must have instances at this size. |
| font-family (.vf-wave-char) | Inter, sans-serif | Must be a variable font with a wght axis for smooth interpolation. Alternatives: Roboto Flex, Plus Jakarta Sans, Nunito Variable. |
FAQ
font-weight interpolation requires a variable font with a wght axis. Without one (e.g. static Inter or Arial), the browser snaps between available weights (400 → 700) instead of interpolating. Load Inter Variable via Google Fonts (@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap')) to get the continuous gradient.<span class="vf-wave-char"> per letter inside your .vf-wave-text, starting with animation-delay: 0s on the first letter and adding 0.15 s per character. Spaces can remain as plain HTML text without the class — they will not be animated.animation-delay: 0s to the last letter and increase towards the first. For a centre-out wave: delay 0 s on the middle letters, increasing towards both ends. No global CSS change is needed.