Wave Form
Continuously animated SVG wave via dual harmonic synthesis: two sine curves at offset frequencies, recomputed frame by frame via requestAnimationFrame.
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. Atmosphere has 57 effects, including 10 free. Explore the category →
3 usage examples



How it works
The effect redraws the SVG path on every frame: animateWaveForm(time) builds a string of L commands from x=0 to x=300 (step 5 px, 61 points) and writes the result via wavePath.setAttribute('d', d). Unlike CSS animation or canvas, this is direct DOM manipulation — the browser repaints the <path> without creating a new element or allocating a buffer.
The shape is the superposition of two sine waves: a primary one (amplitude 30 px, spatial frequency 0.05 rad/px, temporal speed 0.005 rad/ms) and a slower, wider secondary one (amplitude 15 px, frequency 0.02, speed 0.003). Each wave's phase advances with time × speed (time = rAF timestamp in ms), creating the illusion of continuous rightward scrolling without any explicit loop.
The SVG uses viewBox="0 0 300 100" and preserveAspectRatio="none": the wave stretches to fill any container width without additional JS. The stroke color is stroke: url(#waveGradient) in the CSS — the gradient definition is absent from the native HTML and must be added manually inside a <defs> block within the <svg> for the path to be visible.
The requestAnimationFrame loop is shared with other page effects (audio visualizer, matrix rain…): animateWaveForm is one of ten calls in a central animate(time) function. No IntersectionObserver or pause mechanism is implemented — the RAF runs continuously even when the element is off-screen.
Accessibility
- prefers-reduced-motion not supported: the animation runs in the main RAF loop without checking the system preference — implement manually if needed.
- The
<svg>has noaria-hiddenattribute: screen readers traverse it even when the content is purely decorative. Addaria-hidden="true"on the<svg>for decorative use. - The
.wave-formcontainer has neitherrole="img"noraria-label— add these if the wave carries semantic information (e.g. a data curve). - No keyboard or pointer interaction: the effect is passive, no
tabindexor event handler is attached. - The stroke color references
url(#waveGradient)which is absent from the HTML: the path may be invisible without the<defs>block — this gives an empty background but causes no errors or accessibility issues.
Browser compatibility
Requires SVG 1.1 (path + linearGradient) and requestAnimationFrame — natively supported in all modern browsers. Zero external dependencies.
If SVG is not rendered or the gradient is not defined, the <code>.wave-form</code> container stays empty with no JS error — the page remains functional.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="demo-preview">
<div class="wave-form" id="waveForm">
<svg viewBox="0 0 300 100" preserveAspectRatio="none">
<!-- gradient absent from native code: add to make the stroke visible -->
<defs>
<linearGradient id="waveGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#6366f1"/>
<stop offset="50%" stop-color="#d946ef"/>
<stop offset="100%" stop-color="#06b6d4"/>
</linearGradient>
</defs>
<path id="wavePath"></path>
</svg>
</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 |
|---|---|---|
| url(#waveGradient) — SVG defs | not defined in native HTML | Add <defs><linearGradient id="waveGradient">…</linearGradient></defs> inside the <svg> before the <path> — without it the stroke is invisible. |
| stroke-width (CSS .wave-form path) | 3 | Stroke thickness in viewBox px. 1–2 for a subtle watermark; 5–8 for a graphic fill. |
| stroke-linecap (CSS .wave-form path) | round | End-cap style: butt (flush square), square (protruding square), round (rounded). Affects visual smoothness at the path ends. |
| Primary amplitude: 30 (JS animateWaveForm) | 30 | Main wave height in viewBox units (0–100). Lower to 10–15 for a subtle background; raise to 40 for a pronounced wave touching the edges. |
| Secondary amplitude: 15 (JS animateWaveForm) | 15 | Height of the superimposed slow oscillation. Set to 0 for a pure sine; match the primary amplitude for a more complex trace. |
| Primary temporal speed: 0.005 (JS time * 0.005) | 0.005 | Main wave scroll speed (rad/ms). 0.002 = slow and calming; 0.01 = fast and energetic. |
| Secondary temporal speed: 0.003 (JS time * 0.003) | 0.003 | Low-frequency drift speed. Keep lower than the primary speed for a natural, organic motion. |
| Spatial step: 5 (JS for x += 5) | 5 | Point density along the path. Reduce to 2 for a smoother curve (more DOM updates); increase to 10 to lighten fast animations. |
FAQ
stroke: url(#waveGradient) but the <linearGradient id="waveGradient"> is not included in the JSON's HTML. Without a <defs> block in the SVG, the browser can't resolve the color and doesn't render the stroke. Add <defs><linearGradient id="waveGradient" x1="0%" x2="100%"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="#06b6d4"/></linearGradient></defs> inside the <svg> before the <path>.animateWaveForm targets document.getElementById('wavePath') — a single element. For multiple curves, duplicate the <path> with distinct IDs (wavePath2, etc.), create a variant of the function for each ID, and call all variants from the existing requestAnimationFrame loop. Vary amplitudes and speeds to visually distinguish each channel.#1e3a8a → #7c3aed → #0f766e for example. Avoid pastels close to white — SVG transparency provides no built-in backing. Also set stroke-width: 2 minimum to maintain legibility.