Installation
Dynamoblobs is a dependency-free custom element. Your authored <dynamo-blob> stays in the document; the package adds an SVG silhouette inside it when the element upgrades.
npm
npm install dynamoblobsImport once from the browser entry point:
import 'dynamoblobs';<dynamo-blob
data-blob-points="10"
style="display:block;width:120px;height:120px;fill:rebeccapurple"
></dynamo-blob>The package exports the same named runtime API to ESM and CommonJS:
import { DynamoBlob, generateBlobPath, encodeBlobSeed } from 'dynamoblobs';
const { DynamoBlob: Blob } = require('dynamoblobs');Importing the module is SSR-safe. It evaluates without HTMLElement, document, or customElements; browser registration happens only when a custom-element registry exists. DOM queries and instance methods still belong in a client lifecycle.
Frameworks
Write <dynamo-blob> directly in a template, then wait for its definition before calling methods:
import 'dynamoblobs';
import type { DynamoBlob } from 'dynamoblobs';
await customElements.whenDefined('dynamo-blob');
document.querySelector<DynamoBlob>('dynamo-blob')?.generateNewBlob(500);For an identical first silhouette across client sessions, keep a recorded data-blob-seed. Without one, the element deliberately creates its initial shape on upgrade.
Direct script
Direct-script consumers can keep using the UMD files:
<script src="/path/to/dynamoblobs.min.js"></script>
<script>
const path = Dynamoblobs.generateBlobPath({ points: 10, variance: 14 });
</script>The UMD bundle registers <dynamo-blob> and exposes the helper API as globalThis.Dynamoblobs. The explicit ./dist/dynamoblobs.js and ./dist/dynamoblobs.min.js package exports remain for this use case.
Usage
The host controls layout. Give it an explicit or layout-derived size; the internal SVG scales into that box and inherits the host’s fill.
<dynamo-blob style="display:block;width:120px;height:120px;fill:slateblue"></dynamo-blob>Classes, IDs, inline styles, and data attributes all remain on <dynamo-blob>. This is deliberately not a generated-SVG selector contract: style the host.
Shape, deterministic snapshots, and motion
data-blob-points changes the silhouette’s vertices and data-blob-variance controls how far they move from the base radius. Supply either a readable data-blob-seed or a previously encoded path for a reproducible silhouette. A readable seed remains authored so later generations stay deterministic; when no seed is supplied, the element records the generated path as an encoded snapshot.
<dynamo-blob
data-blob-points="12"
data-blob-variance="18"
data-blob-seed="docs-hero-v1"
data-blob-morph-autoplay="true"
style="display:block;width:140px;height:140px;fill:slateblue"
></dynamo-blob>Use the declarative *-autoplay attributes for ambient motion. They respect reduced-motion preferences. Explicit play*() calls are intentional user actions and run even when reduced motion is requested.
Live controls
The controls exercise regeneration, wobble, morphing, drift, and deflection. Wobble starts pressed because it is the default ambient motion layer; each motion button controls its layer independently. The controls wait for the custom element to register, expose disabled loading states, and announce every result.
Preparing the live blob controls.
Updating a connected blob
Every documented configuration or reflected state attribute is observed. Change a value after upgrade without replacing the host:
await customElements.whenDefined('dynamo-blob');
const blob = document.querySelector('dynamo-blob');
blob.dataset.blobPoints = '14';
blob.dataset.blobMorphAutoplay = 'true';
blob.dataset.blobDriftClick = 'true';Geometry changes retarget the visible SVG in place; motion settings are reconfigured without replacing the host. A drifting blob needs a positioned parent with practical dimensions. data-blob-drift-click="true" exposes the host as a focusable, named button that deflects on pointer click, Enter, or Space. Add an aria-label when the default “Deflect blob” label is not specific enough.
API reference
Attributes
All observed attributes are strings in markup. This reference keeps the historic anchors and covers the full current attribute surface: shape and observation, wobble, morph, drift, and the four reflected state properties.
| Attribute | Default | Meaning |
|---|---|---|
data-blob-points | 10 | Vertex count; values below three are clamped. |
data-blob-variance | 8 | Radius deviation in the internal 100-unit viewBox. |
data-blob-seed | random | Readable deterministic key or encoded snapshot. |
data-blob-observe | unset | Regenerate on viewport exit: once, continuous, optionally with :margin. |
data-blob-wobble-autoplay | true | Start ambient CSS wobble after upgrade. |
data-blob-wobble-speed | 30000 | Wobble period in milliseconds. |
data-blob-wobble-intensity | 2 | Wobble deformation multiplier. |
data-blob-morph-autoplay | false | Start the continuous morph loop. |
data-blob-morph-speed | 7500 | Morph-loop cycle duration in milliseconds. |
data-blob-morph-intensity | 1 | RMS fraction of variance moved each cycle. |
data-blob-morph-tween | 600 | Transition duration in milliseconds when live shape attributes retune the blob. |
data-blob-drift-autoplay | false | Start DVD-style drift in the positioned parent. |
data-blob-drift-speed | 1.25 | Drift speed multiplier. |
data-blob-drift-intensity | 1 | Turn severity for collisions and deflect(). |
data-blob-drift-bias | 0.9 | Collision-radius multiplier, clamped from 0.5 to 1.5. |
data-blob-drift-click | false | Expose the host as a button that deflects on click, Enter, or Space. |
data-blob-drift-start-position | random | random, center, or current. |
data-blob-is-wobbling | reflects state | Set true/false or read the live wobble state. |
data-blob-is-morphing | reflects state | Set true/false or read the live morph state. |
data-blob-is-drifting | reflects state | Set true/false or read the live drift state. |
data-blob-is-animating | reflects state | Master control; true when any layer is playing. |
The seventeen configuration attributes control the blob; the four data-blob-is-* attributes mirror live state and are writable controls. This distinction keeps declarative setup separate from runtime state.
State properties
The element exposes four readonly booleans: .isWobbling, .isMorphing, .isDrifting, and .isAnimating. Each mirrors its matching data-blob-is-* attribute. Use the master state when a UI needs one pause/resume switch, and the layer properties for a specific control.
Instance methods
Every instance method returns the element, so layered controls can chain. The ten methods are play(options?), pause(), playWobble(durationMs?), pauseWobble(), playMorph(customDuration?), pauseMorph(), playDrift(speed?), pauseDrift(), generateNewBlob(duration?), and deflect().
const blob = document.querySelector('dynamo-blob');
blob.pauseWobble().playMorph(4000).playDrift(1.5);
blob.generateNewBlob(500);
blob.deflect();play() resumes configured auto-play layers. Passing { morph, wobble, drift } forces each listed layer and supplies its timing. pause() freezes all three layers in place. generateNewBlob() morphs from the exact visible shape to a new silhouette. If continuous morphing is active, it resumes smoothly from the generated points; repeated generation requests retarget the in-flight tween instead of being dropped. Reduced-motion mode completes generation with a 1ms transition. deflect() redirects a drifting blob without changing its speed.
Completion event
dynamo-blob-complete fires when a one-off generation or morph cycle completes. It is dispatched on the blob; attach the listener directly.
blob.addEventListener('dynamo-blob-complete', (event) => {
console.log('Blob finished', event.detail);
}, { once: true });Runtime and TypeScript exports
The ten runtime exports are DynamoBlob, generateBlobPath, generateBlobPoints, nextMorphShape, parseBlobPath, interpolateBlob, resampleClosed, createSeededRandom, encodeBlobSeed, and decodeBlobSeed.
import {
createSeededRandom, generateBlobPath, encodeBlobSeed, decodeBlobSeed,
parseBlobPath, nextMorphShape
} from 'dynamoblobs';
const random = createSeededRandom('avatar-42');
const path = generateBlobPath({ points: 9, variance: 14, random });
const seed = encodeBlobSeed(path);
const restored = decodeBlobSeed(seed);
const next = nextMorphShape(path, { variance: 14, intensity: 0.5 });The declarations export BlobPoint, BlobGenerationOptions, NextMorphShapeOptions, BlobPlayOptions, DynamoBlob, and DynamoBlobAttributes, and add <dynamo-blob> to HTMLElementTagNameMap and JSX intrinsic elements.
Lifecycle, SSR, and accessibility
- Generated SVG is decorative. The host is also hidden from assistive technology unless
data-blob-drift-clickturns it into a keyboard-operable button. Supplyaria-labeloraria-labelledbywhen “Deflect blob” is not enough context. - Automatic animation honors
prefers-reduced-motion; the demo offers explicit controls instead of assuming motion is required. - The module is safe to import without DOM globals. The browser registry guards duplicate definition.
- Disconnecting the host cancels active animation frames and observation; reconnecting rebuilds the element state.
IntersectionObserveris only necessary fordata-blob-observe; the silhouette still renders where it is unavailable.
Troubleshooting
- The blob has no useful space: set width and height on the host or place it in a sizing layout.
- The blob is invisible: give the host a
fillthat contrasts with its surface. - A method is missing: import the package and wait for
customElements.whenDefined('dynamo-blob')before calling it. - Drift does not move: give its parent a practical positioned box, then enable
data-blob-drift-autoplayor callplayDrift().
Practical examples
Blobs can do all kinds of things more than just sitting around looking squishy. Especially when you throw clipPath into the mix. First though, blobs that make a living just sitting around.
Keep an attention grabber fresh
Build a small visual system where blobs can live without obscuring relevant content. Different point counts, variance, and slow morph/wobble speeds keep the effect looking curated without every becoming stale.
<section class="campaign-cover">
<div class="campaign-art" aria-hidden="true">
<dynamo-blob data-blob-points="8" data-blob-variance="8"
data-blob-morph-autoplay="true" data-blob-morph-speed="11000" data-blob-wobble-autoplay="false"></dynamo-blob>
<dynamo-blob data-blob-points="12" data-blob-variance="14"
data-blob-morph-autoplay="true" data-blob-morph-speed="28000" data-blob-wobble-speed="90000"></dynamo-blob>
<dynamo-blob data-blob-points="8" data-blob-variance="20"
data-blob-morph-autoplay="true" data-blob-wobble-speed="80000" data-blob-morph-speed="16500"></dynamo-blob>
</div>
<div class="campaign-copy">
<p>Field notes · 04</p>
<h2>Make space for some squishies.</h2>
</div>
</section>Make space for some squishies.
Layer a few low-speed blobs into a repeatable art direction, then allow them to breathe.
The generative cover is ready.
Blob-shaped images!
Use the public path helpers to drive an SVG clipPath, then place any image behind it. The image remains accessible and reusable; only the crop geometry morphs. Hold the first deterministic path when the visitor prefers reduced motion.
import {
createSeededRandom, generateBlobPath, interpolateBlob,
nextMorphShape, parseBlobPath,
} from 'dynamoblobs';
const random = createSeededRandom('studio-crop');
let currentPath = generateBlobPath({ points: 9, variance: 17, random });
function drawMorph(progress, targetPath) {
const from = parseBlobPath(currentPath);
const target = parseBlobPath(targetPath);
clipPath.setAttribute('d', interpolateBlob(from, target, progress));
}
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
const targetPath = nextMorphShape(currentPath, {
variance: 17,
intensity: 0.48,
random,
});
// Call drawMorph() from requestAnimationFrame with an eased 0–1 progress.
}<svg viewBox="0 0 100 100" role="img" aria-labelledby="studio-title">
<title id="studio-title">A ceramic artist's sunlit studio</title>
<defs>
<clipPath id="studio-crop"><path id="clip-path"></path></clipPath>
</defs>
<image href="studio.jpg" width="100" height="100"
preserveAspectRatio="xMidYMid slice" clip-path="url(#studio-crop)"></image>
</svg>Field Notes · 06
Don't be afraid to get gooey.
The source image stays rectangular and useful. Only the SVG clip path changes, so the same asset can still serve ordinary image layouts elsewhere.
Organic-shaped avatars
A single, slowly morphing blob can pretty easily become the background for all sorts of things. Things like this avatar, where the initials remain ordinary text above a decorative blob which insures proper accesibility for screen readers.
<article class="maker-profile">
<div class="maker-avatar" aria-hidden="true">
<dynamo-blob data-blob-points="9" data-blob-variance="16"
data-blob-morph-autoplay="true" data-blob-morph-speed="11000"></dynamo-blob>
<span>NA</span>
</div>
<div>
<p>Featured maker</p>
<h2>Nia Alvarez</h2>
<p>Turns discarded clay into quiet, useful objects.</p>
</div>
</article>Featured maker
Nia Alvarez
Turns discarded clay into quiet, useful objects from a one-room studio in Santa Fe.
- Wheel-thrown
- Reclaimed clay
- Small batch
Interactive animation made easy
Use a one-off morph to visualize feedback as an interface advances. Disable the trigger during the transition and return focus when the next state is ready.
const blob = document.querySelector('#process-blob');
const nextButton = document.querySelector('#next-step');
nextButton.addEventListener('click', () => {
nextButton.disabled = true;
blob.addEventListener('dynamo-blob-complete', () => {
nextButton.disabled = false;
nextButton.focus();
}, { once: true });
blob.generateNewBlob(560);
});Collect: Find the signal. Step 1 of 3.