Installation
Dynamowaves is a dependency-free custom element. It keeps the authored <dynamo-wave> host in the document and renders a generated SVG path inside it.
npm
npm install dynamowavesA side-effect import is enough when you only need the element:
import 'dynamowaves';Then author the element in ordinary HTML and give its host a visible height and fill:
<dynamo-wave class="hero-wave" data-wave-points="8"></dynamo-wave>
<style>
.hero-wave {
display: block;
width: 100%;
height: 5rem;
fill: rebeccapurple;
}
</style>The package also provides named ESM exports:
import { DynamoWave, generateWave, encodeWaveSeed } from 'dynamowaves';CommonJS receives the same runtime API:
const { DynamoWave, generateWave, encodeWaveSeed } = require('dynamowaves');Importing the package is safe during SSR: it does not require browser globals to evaluate. The custom element registers only in a browser environment with customElements; its SVG is created after client-side upgrade.
Bundlers and frameworks
Import dynamowaves once from the browser entry point used by your application, then use <dynamo-wave> directly in HTML, JSX, or a framework template. Attributes remain strings in markup; use an element reference for generateNewWave(), play(), and pause().
If framework code may run before the custom element has upgraded, wait for its definition before calling methods:
import 'dynamowaves';
import type { DynamoWave } from 'dynamowaves';
await customElements.whenDefined('dynamo-wave');
const wave = document.querySelector<DynamoWave>('dynamo-wave');
wave?.generateNewWave(500);Server-rendered frameworks may emit the <dynamo-wave> host normally. Importing the module during SSR is safe, but method calls and DOM queries still belong in the framework’s client lifecycle. For an identical server and first-client shape, render a previously recorded data-wave-seed; otherwise the SVG is generated when the element upgrades.
Direct script or CDN
The UMD files remain available for projects that load scripts directly. They register <dynamo-wave> and expose the helper API as globalThis.Dynamowaves.
<!-- Local copy -->
<script src="/path/to/dynamowaves.min.js"></script>
<!-- Latest compatible 2.x release from npm through jsDelivr -->
<script
src="https://cdn.jsdelivr.net/npm/dynamowaves@2/dist/dynamowaves.min.js"
crossorigin="anonymous"
></script>
<script>
const path = Dynamowaves.generateWave({
width: 1440,
height: 160,
points: 6,
variance: 3,
});
</script>Replace @2 with an exact published version when a deployment must not move to a newer compatible release automatically.
Angular
After installing from npm, add the browser bundle to the scripts array in angular.json:
"scripts": [
"node_modules/dynamowaves/dist/dynamowaves.js"
]Then enable custom-element markup in the owning module:
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
@NgModule({
// ...
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}Usage
Add the custom element anywhere a responsive SVG wave belongs:
<dynamo-wave
style="display:block;height:5rem;fill:rebeccapurple"
></dynamo-wave>Without configuration, the wave faces top, uses six points and variance 3, and renders one static shape. Add data-wave-animate="true" for continuous motion or call the public methods when motion should respond to an interaction.
Shape and direction
Use data-wave-face to choose the edge the filled portion occupies, data-wave-points to change the number of anchors, and data-wave-variance to control their deviation. data-start-end-zero is useful when the wave must join a straight edge cleanly.
<dynamo-wave
data-wave-face="bottom"
data-wave-points="8"
data-wave-variance="2.5"
data-start-end-zero
style="display:block;height:5rem;fill:slateblue"
></dynamo-wave>The complete accepted values, defaults, and live-update behavior are in the
Basic styling
The <dynamo-wave> host remains in the document, so its class, id, and style stay available for layout and styling. Set a height on the host and use fill to color the generated path.
<!-- Example 1 -->
<dynamo-wave style="fill:slateblue"></dynamo-wave>
<!-- Example 2 -->
<style>
.fill-theme {
fill: var(--zbk-brand-canvas);
}
</style>
<dynamo-wave class="fill-theme"></dynamo-wave>
<!-- Example 3 -->
<style>
#special_wave {
height: 3rem;
width: 80%;
transform: translateX(10%);
}
</style>
<dynamo-wave id="special_wave" class="fill-theme fill-light"></dynamo-wave>The SVG stretches to the host with preserveAspectRatio="none". That makes the wave responsive, but it also means the host’s dimensions are the layout contract. Style the host rather than the generated svg or path children.
Updating a connected wave
All nine public attributes are observed. Changing one after upgrade updates the element without replacing it:
await customElements.whenDefined('dynamo-wave');
const wave = document.querySelector('dynamo-wave');
wave.dataset.wavePoints = '10';
wave.dataset.waveFace = 'left';
wave.dataset.waveAnimate = 'true';Geometry changes rebuild the wave. Speed, automatic animation, and viewport observation are reconfigured in place; an active loop resumes when a geometry rebuild finishes.
Playing with motion
Use generateNewWave() for a one-off morph, or play() and pause() for a continuous loop. The interactive examples below use the same public methods described in the
Wave controls ready.
API reference
Version 2.2.0 exposes nine observed attributes, three instance methods, one completion event, and six runtime module exports. Attribute changes take effect after the element is connected: geometry changes rebuild the wave, while speed, animation, and observation changes update their active behavior.
Attributes
| Attribute | Default | Accepted values | Runtime behavior |
|---|---|---|---|
data-wave-face | top | top, bottom, left, or right. Any other value behaves like top. | Rebuilds the SVG with horizontal or vertical geometry and the requested orientation. |
data-wave-points | 6 | A numeric string. It is parsed as an integer and clamped to a minimum of 2; invalid values use the default. | Regenerates the current and target paths with the new point count. |
data-wave-variance | 3 | Any finite number, including decimals. Positive values are the useful range for ordinary wave depth. | Regenerates the wave with the new amount of anchor deviation. |
data-variance | Unset | Legacy alias for data-wave-variance. The preferred attribute wins when both are present. | Regenerates the wave. Retained for existing markup. |
data-wave-speed | 7500 | A finite number greater than zero, in milliseconds. Invalid values use the default. | Updates a stopped wave immediately. A running loop restarts from the displayed shape at the new speed. |
data-wave-animate | false | The exact string true starts the loop; any other value stops it. | Starts or pauses continuous morphing, subject to the viewer's reduced-motion preference. |
data-wave-observe | Unset | once or repeat, optionally followed by an IntersectionObserver root margin such as repeat:100px. | Replaces the active observer. Regeneration occurs when the wave is outside the adjusted viewport. |
data-wave-seed | Generated | An encoded path snapshot produced by Dynamowaves, or any nonempty string for deterministic generation. | Rebuilds the wave from the supplied seed. Every render reflects its resulting path back to this attribute. |
data-start-end-zero | False | An empty value, true, 1, yes, or on enables it. Any other value disables it. | Regenerates the wave with both visible endpoints anchored to the base edge. |
Geometry changes discard an existing encoded path snapshot because it no longer describes the requested shape. If the wave was looping, it resumes after rebuilding.
<dynamo-wave
data-wave-face="bottom"
data-wave-points="8"
data-wave-variance="2.5"
data-start-end-zero
></dynamo-wave>Recorded and deterministic seeds
Dynamowaves supports two seed forms:
- Recorded path: after every render, the element writes an unpadded Base64 representation of its current SVG path to
data-wave-seed. Copy that value when another wave must use the exact same shape. This is ordinary Base64, not a URL-safe encoding. - Deterministic string: a nonempty value that is not an encoded Dynamowaves path seeds the internal random-number generator. After rendering, the string is replaced by the recorded path it produced.
<dynamo-wave id="hero-wave" data-wave-seed="homepage-hero-v1"></dynamo-wave>
<script>
const hero = document.getElementById('hero-wave');
const footer = document.createElement('dynamo-wave');
// Read after the hero has connected so this is its recorded path.
footer.dataset.waveSeed = hero.dataset.waveSeed;
document.body.appendChild(footer);
</script> If you later change a geometry attribute, reapply the original deterministic string if you want that new geometry to remain tied to the same human-readable seed.
Viewport observation
data-wave-observe="once" regenerates on the first non-intersecting observation and then disconnects. repeat stays connected and regenerates whenever the observer reports the element outside its root.
<!-- Regenerate once when outside the viewport. -->
<dynamo-wave data-wave-observe="once"></dynamo-wave>
<!-- The positive margin expands the observed viewport, so the wave must move
farther away before it becomes non-intersecting. -->
<dynamo-wave data-wave-observe="repeat:100px"></dynamo-wave>
<!-- A negative margin contracts it, so non-intersection happens sooner. -->
<dynamo-wave data-wave-observe="once:-50px"></dynamo-wave>The margin uses the same syntax as IntersectionObserver.rootMargin, including multi-value forms such as 100px 0px. If IntersectionObserver is unavailable, Dynamowaves warns once for that setup and leaves viewport regeneration disabled; the wave itself still renders.
JavaScript API
| Method | Behavior | While another animation is active |
|---|---|---|
generateNewWave(duration = 800) | Morphs once from the displayed path to a new path. Durations below one millisecond, and reduced motion, resolve in one millisecond. | Ignored while another morph or animation frame is active. |
play(duration?) | Starts continuous morphing. A finite positive duration overrides data-wave-speed for that loop. | Ignored while already playing, while a one-off morph is active, or while reduced motion is enabled. |
pause() | Stops a loop and preserves its current tween position for a later play(). | Cancels an active one-off morph and clears that morph's timeline. |
All three methods return void. Use the completion event rather than internal state such as isAnimating when sequencing work.
const wave = document.querySelector('dynamo-wave');
wave.addEventListener('dynamo-wave-complete', () => {
console.log('The requested morph finished.');
}, { once: true });
wave.generateNewWave(500);dynamo-wave-complete
The element dispatches dynamo-wave-complete after a one-off morph and after every completed cycle of play(). It is a non-bubbling, non-composed CustomEvent.
wave.addEventListener('dynamo-wave-complete', (event) => {
event.detail.duration; // number, milliseconds
event.detail.direction; // 'horizontal' | 'vertical'
});The event reports orientation rather than the authored face: top and bottom are horizontal; left and right are vertical.
Module and helper exports
Importing the package registers <dynamo-wave> when a custom-element registry is available. The same entry point also exposes the component class and its lower-level path helpers.
import {
DynamoWave,
generateWave,
parsePath,
interpolateWave,
encodeWaveSeed,
decodeWaveSeed,
} from 'dynamowaves';| Export | Purpose |
|---|---|
DynamoWave | The custom-element class registered as dynamo-wave in browser environments. |
generateWave(options) | Creates a complete SVG path string from width, height, point, variance, orientation, random-source, and endpoint options. |
parsePath(path) | Extracts the quadratic control points and endpoints used by the interpolator. |
interpolateWave(current, target, progress, vertical, height, width) | Builds the path for one interpolation progress value between compatible point arrays. Orientation and dimensions are required so the returned path closes against the correct base edge. |
encodeWaveSeed(path) | Normalizes a path and returns its unpadded Base64 snapshot, or an empty string when it cannot encode. |
decodeWaveSeed(seed) | Returns a validated Dynamowaves path or null. Plain deterministic seed strings intentionally return null. |
These helpers are available from both ESM and CommonJS. A direct browser script exposes the same names on globalThis.Dynamowaves.
generateWave options
generateWave() is the low-level geometry API. It does not create or modify DOM; it returns a closed SVG path string.
| Option | Required | Purpose |
|---|---|---|
width | Yes | Numeric width used by the generated coordinate system. |
height | Yes | Numeric height used by the generated coordinate system. |
points | Yes | Anchor count. Finite values are floored and clamped to at least 2. |
variance | Yes | Multiplier controlling how far randomized anchors can deviate across the wave depth. |
vertical | No | false for top/bottom geometry; true for left/right geometry. |
random | No | A function returning a number. Defaults to Math.random; inject a seeded or fixed source for repeatable output. |
startEndZero | No | Anchors both visible endpoints to the base edge when true. |
import { generateWave } from 'dynamowaves';
// A fixed sequence keeps the example repeatable without flattening the wave.
const samples = [0.25, 0.85, 0.15, 0.7, 0.35, 0.9, 0.2, 0.6];
let sample = 0;
const path = generateWave({
width: 1440,
height: 160,
points: 8,
variance: 2.5,
random: () => samples[sample++ % samples.length],
startEndZero: true,
});
document.querySelector('#generated-wave path').setAttribute('d', path); Parsing and interpolating paths
parsePath() understands the quadratic path format emitted by Dynamowaves. interpolateWave() expects two parsed arrays with the same number of segments, a progress value from 0 to 1, the orientation, and the same height and width used to generate the paths.
import { generateWave, interpolateWave, parsePath } from 'dynamowaves';
const options = { width: 1440, height: 160, points: 6, variance: 3 };
const sequence = (samples) => {
let sample = 0;
return () => samples[sample++ % samples.length];
};
const startPath = generateWave({
...options,
random: sequence([0.15, 0.8, 0.35, 0.95, 0.25, 0.65]),
});
const endPath = generateWave({
...options,
random: sequence([0.75, 0.45, 0.9, 0.2, 0.6, 0.1]),
});
const halfwayPath = interpolateWave(
parsePath(startPath),
parsePath(endPath),
0.5,
false,
options.height,
options.width,
); Malformed or unrelated path strings produce an empty array from parsePath(). The helpers do not repair mismatched point arrays; validate compatibility before calling interpolateWave().
Encoding and decoding snapshots
import { decodeWaveSeed, encodeWaveSeed } from 'dynamowaves';
const seed = encodeWaveSeed(path);
const restoredPath = decodeWaveSeed(seed); // string
const plainSeed = decodeWaveSeed('homepage-hero-v1'); // nulldecodeWaveSeed() only accepts encoded strings that decode to the Dynamowaves path format. Returning null for a plain deterministic seed is expected; the custom element, rather than this helper, turns that string into a seeded random source.
TypeScript exports
The package declarations export DynamoWave, DynamoWaveAttributes, DynamoWaveCompleteDetail, DynamoWaveEventMap, WaveDirection, WaveGenerationOptions, WaveObserverOptions, WaveOrientation, and WavePoint. They also add <dynamo-wave> to HTMLElementTagNameMap, type the completion event on HTMLElementEventMap, and provide the custom element’s JSX attributes.
Styling and layout
The custom-element host stays in the document. Its id, classes, inline styles, and data attributes remain on that host; Dynamowaves renders a light-DOM SVG inside it.
<dynamo-wave class="section-wave"></dynamo-wave>
<style>
.section-wave {
display: block;
width: 100%;
height: 5rem;
fill: rebeccapurple;
stroke: transparent;
}
</style>The generated SVG fills the host with width: 100%, height: 100%, and preserveAspectRatio="none". Its path inherits fill and stroke; text color alone does not set the wave fill. When an unstyled host computes to display: inline, Dynamowaves gives it an inline display: block default. An authored display value wins.
Style the host rather than depending on the generated svg and path structure as a selector contract.
Lifecycle, SSR, and accessibility
- The module can be imported during SSR or in Node without
HTMLElementorcustomElements. Registration happens only where a browser custom-element registry exists. - Server output contains the authored
<dynamo-wave>host; the SVG is created when the element upgrades in the browser. Reuse a recordeddata-wave-seedwhen the first client-rendered shape must be identical across pages or environments. - The generated SVG is decorative: it uses
aria-hidden="true"androle="presentation". Do not use the wave as the only carrier of meaningful information. prefers-reduced-motion: reduceprevents continuous playback and reduces one-off morphs to one millisecond. A running authored loop pauses when the preference changes live and resumes when motion is allowed again, unless animation was explicitly disabled in the meantime.- Removing an active element cancels its animation frame and disconnects its observers. Reattaching it resumes a loop that had been running; an interrupted one-off morph does not resume.
- Changing geometry while a loop is active rebuilds the paths and resumes the loop with the new configuration.
- The baseline browser requirements are Custom Elements and
requestAnimationFrame.IntersectionObserveris only required fordata-wave-observe; the wave still renders when observation is unavailable.
Troubleshooting
- The element takes up no useful space: give the
<dynamo-wave>host an explicit or layout-derived height. Its generated SVG isheight: 100%and cannot invent the surrounding layout. - The wave is present but invisible: set
fillon the host and check that it is not the same color as the surface behind it.coloralone does not set the path fill. - A method is undefined: make sure the package was imported, then wait for the custom element definition before calling instance methods. The framework example above shows the exact
customElements.whenDefined()pattern. - A parent listener never sees completion:
dynamo-wave-completedoes not bubble or cross a shadow boundary. Attach the listener directly to the<dynamo-wave>element. - A readable seed string changes after render: that is expected. The element replaces it with the encoded snapshot it generated. Save the original string separately if you need to reapply it after changing geometry.
- Animation does not start:
data-wave-animateonly accepts the exact stringtrue, and continuous animation is intentionally disabled while the viewer requests reduced motion. - Viewport regeneration feels early or late: positive
data-wave-observemargins expand the observed viewport and delay non-intersection; negative margins contract it and make non-intersection happen sooner.
Practical examples
Dynamowaves supplies geometry and motion while your layout supplies the size, color, and placement. These recipes use only the public host attributes, methods, and completion event.
Sticky header divider
Keep a labeled header pinned inside a scrolling panel and let a bottom-facing wave soften its edge:
<div class="scrolling-panel">
<div class="panel-header">
<h2>Wave settings</h2>
<dynamo-wave
data-wave-face="bottom"
style="display:block;height:3rem;fill:var(--header-background)"
></dynamo-wave>
</div>
<div class="panel-content">...</div>
</div>
<style>
.scrolling-panel {
--header-background: #dbeafe;
max-height: 24rem;
overflow: auto;
}
.panel-header {
position: sticky;
top: 0;
background: var(--header-background);
}
</style>Event-driven transition
Pair generateNewWave() with the completion event when interface state should settle after a morph. Listen on the wave itself because the event does not bubble.
<dynamo-wave
id="transition-wave"
style="display:block;height:4rem;fill:rebeccapurple"
></dynamo-wave>
<button id="next" type="button">Next</button>const wave = document.querySelector('#transition-wave');
const nextButton = document.querySelector('#next');
nextButton.addEventListener('click', () => {
nextButton.disabled = true;
wave.addEventListener('dynamo-wave-complete', () => {
nextButton.disabled = false;
nextButton.focus();
}, { once: true });
wave.generateNewWave(500);
});Responsive image-edge divider
Place a vertical wave over the edge of an image or visual panel. A small negative overlap prevents a one-pixel seam while the layout resizes.
<div class="feature-card">
<div class="feature-image">
<img src="/path/to/image.jpeg" alt="A school of fish" />
<dynamo-wave
data-wave-face="left"
style="fill:var(--card-background)"
></dynamo-wave>
</div>
<div class="content">...</div>
</div>
<style>
.feature-card {
--card-background: white;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(12rem, 1fr);
background: var(--card-background);
}
.feature-image {
position: relative;
min-height: 16rem;
}
.feature-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.feature-image dynamo-wave {
position: absolute;
inset: -1px -1px -1px auto;
width: 3rem;
}
</style>