Smooth, configurable SVG animations for decorative backgrounds and section dividers.
~3KB gzipped. Zero dependencies. TypeScript-first.
- Anchor to any edge — fill from the top, bottom, left, or right of the container
- Fully configurable — speed, colors, wave components, segment resolution, and more
- Plays well with CSS — uses
currentColorby default so it inherits your theme - Framework-agnostic — vanilla DOM; works with React, Vue, Svelte, or plain HTML
npm install cymaOr use a CDN — no build step required:
<script type="module">
import { wave } from "https://esm.sh/cyma";
wave(document.getElementById("hero"));
</script><div id="hero"></div>import { wave } from "cyma";
const animation = wave(document.getElementById("hero")!);
// animation.stop(), animation.start(), animation.destroy()
// See "Animation controls" below.Requires a browser with SVG support. For SSR frameworks (Next.js, Nuxt), run Cyma only on the client side.
All animations accept these options:
| Option | Type | Default | Description |
|---|---|---|---|
speed |
number |
1 |
Global speed multiplier |
fill |
string |
'currentColor' |
SVG fill color ('currentColor' inherits from the parent's CSS color) |
anchor |
'top' | 'bottom' | 'left' | 'right' |
'top' |
Edge the filled region clings to |
viewBox |
{ width, height } |
1440 × 320 |
SVG coordinate space |
height |
string |
— | CSS height of the SVG element. If omitted, scales with the viewBox aspect ratio.* |
* A fixed height that doesn't match the viewBox aspect ratio will stretch the animation non-uniformly. This is most noticeable with bubbles, where round shapes become elliptical.
A traveling wave built from superimposed sine components. The filled region extends from the anchor edge to the wave contour.
wave(document.getElementById("hero")!, {
speed: 1.5,
fill: "#3b82f6",
anchor: "bottom",
});| Option | Type | Default | Description |
|---|---|---|---|
segments |
number |
20 |
Curve resolution (higher = smoother, heavier) |
waves |
WaveComponent[] |
see below | Sinusoidal components to superimpose |
taper |
number |
0 |
Fraction of the width (0–0.5) to smoothly curve down to zero at each edge. 0 = hard borders. |
The default waves preset produces a gentle, layered ocean-like motion from four components with varying amplitudes, frequencies, and directions.
interface WaveComponent {
amplitude: number; // Peak amplitude in viewBox units
frequency: number; // Full cycles across the viewport
phase?: number; // Initial phase offset in radians (default: 0)
speed: number; // Angular velocity in rad/s (negative = travel left)
}Bumps emerge from the anchor edge, morph into semicircles, then detach and float away as fading circles.
The default viewBox is 1440 × 160 (overriding the shared default).
bubbles(document.getElementById("footer")!, {
fill: "#8b5cf6",
anchor: "bottom",
count: 6,
});| Option | Type | Default | Description |
|---|---|---|---|
segments |
number |
12 |
Curve resolution per bump (higher = smoother, heavier) |
count |
number |
8 |
Number of bubbles |
size |
number | { min, max } |
35 |
Bubble radius in viewBox units. A number sets the max (min defaults to 40% of max). |
fadeDuration |
number |
6 |
Maximum fade duration in seconds |
fadeDistribution |
Distribution |
'flat' |
How fade durations are spread across bubbles |
distanceDistribution |
Distribution |
'linear' |
How travel distances are spread across bubbles |
taper |
number |
0 |
Fraction of the width (0–0.5) to taper at each edge. Bump tails curve to zero in the taper zone; bubbles are offset so their semicircles stay outside it. |
Distribution is 'flat' | 'linear' | 'normal':
flat— all bubbles get the same valuelinear— uniform spread (equal chance of any value)normal— bell curve (clusters toward the middle, tapers at extremes)
Every animation function returns an AnimationInstance:
interface AnimationInstance {
readonly svg: SVGSVGElement;
stop(): void;
start(resume?: boolean): void;
destroy(): void;
}svg— Direct reference to the injected SVG element.stop()— Pauses the animation. The SVG remains in the DOM at its last frame.start(resume?)— Resumes the animation. Passtrueto continue from the paused position. The default (false) catches up to the global timeline — the animation snaps to the same frame a never-paused instance would be showing.destroy()— Stops the animation and removes the SVG from the DOM.
Shapes are rendered as cubic Bézier curves via Hermite interpolation, so contours stay smooth at any segment count. All timing derives from Date.now(), meaning separate instances stay in sync regardless of frame rate or tab throttling.
Cyma is written in TypeScript and ships its own type declarations. All option interfaces and the AnimationInstance type are exported:
import type { WaveOptions, BubblesOptions, AnimationInstance } from "cyma";MIT