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 dynamoblobs

Import 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.

New Blob Play wobble Play morph Play drift Deflect

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.

AttributeDefaultMeaning
data-blob-points10Vertex count; values below three are clamped.
data-blob-variance8Radius deviation in the internal 100-unit viewBox.
data-blob-seedrandomReadable deterministic key or encoded snapshot.
data-blob-observeunsetRegenerate on viewport exit: once, continuous, optionally with :margin.
data-blob-wobble-autoplaytrueStart ambient CSS wobble after upgrade.
data-blob-wobble-speed30000Wobble period in milliseconds.
data-blob-wobble-intensity2Wobble deformation multiplier.
data-blob-morph-autoplayfalseStart the continuous morph loop.
data-blob-morph-speed7500Morph-loop cycle duration in milliseconds.
data-blob-morph-intensity1RMS fraction of variance moved each cycle.
data-blob-morph-tween600Transition duration in milliseconds when live shape attributes retune the blob.
data-blob-drift-autoplayfalseStart DVD-style drift in the positioned parent.
data-blob-drift-speed1.25Drift speed multiplier.
data-blob-drift-intensity1Turn severity for collisions and deflect().
data-blob-drift-bias0.9Collision-radius multiplier, clamped from 0.5 to 1.5.
data-blob-drift-clickfalseExpose the host as a button that deflects on click, Enter, or Space.
data-blob-drift-start-positionrandomrandom, center, or current.
data-blob-is-wobblingreflects stateSet true/false or read the live wobble state.
data-blob-is-morphingreflects stateSet true/false or read the live morph state.
data-blob-is-driftingreflects stateSet true/false or read the live drift state.
data-blob-is-animatingreflects stateMaster 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-click turns it into a keyboard-operable button. Supply aria-label or aria-labelledby when “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.
  • IntersectionObserver is only necessary for data-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 fill that 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-autoplay or call playDrift().

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.

Remix the cover

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>
A ceramic artist's sunlit studioThe studio photograph is revealed through a gently morphing blob-shaped crop.

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

Gather the references, fragments, and half-formed ideas worth carrying forward.

Collect: Find the signal. Step 1 of 3.

An idea by Mark Zebley.