Blob Morph
An 8-vertex organic SVG blob deforms in real time against a simulated audio feed — cubic Catmull-Rom curves and CSS drop-shadow glow, no canvas, no dependency.
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 animates a single SVG <path> whose d attribute is recomputed on every requestAnimationFrame tick. Eight vertices are spaced at equal angles around the viewBox center (50, 50) in a 100×100 coordinate space. Each frame assigns each vertex a radius between 30 and 45 SVG units based on the simulated audio signal.
The getSimulatedAudio generator superimposes two sine waves per vertex: sin(t × freq × 0.002) × 0.5 + sin(t × freq × 0.003 + i) × 0.3, plus 20% random noise. Each vertex's own frequency increases by 0.5 per index (baseFreq + i × 0.5, with baseFreq = 0.3) — vertices oscillate at slightly different rates, simulating a multi-band spectral response.
The 8 computed points are joined by cubic Bézier curves whose control points follow a Catmull-Rom formula at tension 0.2: cp1 = p0 + (p1 − p_{i−1}) × 0.2, cp2 = p1 − (p_{i+2} − p0) × 0.2. The loop closes back to the first vertex (Z), guaranteeing a seamless silhouette with no gap or hard corner.
Visual rendering uses fill: url(#blobGradient) (a radial gradient to define in the SVG <defs>) and filter: drop-shadow(rgba(99, 102, 241, 0.5) 0 0 20px) in CSS — the glow tracks the recomputed outline every frame with no canvas redraw cost.
Accessibility
- prefers-reduced-motion absent — the animation loops indefinitely even when the OS signals a reduced-motion preference. Add
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;before the firstrequestAnimationFrame. - The
<svg>and its<path>carry neitheraria-hiddennor a semantic role — screen readers expose the element with a very longdattribute. Addaria-hidden="true"to the SVG. - No keyboard interaction or native focus — the effect responds to the RAF only, no user events are handled or documented.
- The indigo-violet gradient on a dark background yields adequate perceived luminance for a purely decorative element; no text is overlaid in the base demo, so WCAG 1.4.3 does not apply.
- The
#blobGradientis referenced by URL but not defined in the provided snippet — without a<defs>block in the SVG, the fill defaults to black. Always declare the<radialGradient>before use.
Browser compatibility
Requires inline SVG, CSS filter: drop-shadow, and requestAnimationFrame — no canvas, no external dependency.
If JavaScript is disabled, the blob stays fixed in the initial shape defined in the HTML <code>d</code> attribute — no JS error, the SVG silhouette remains visible.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="blob-morph" id="blobMorph">
<svg viewBox="0 0 100 100" aria-hidden="true">
<defs>
<radialGradient id="blobGradient" cx="38%" cy="35%">
<stop offset="0%" stop-color="#a78bfa"/>
<stop offset="100%" stop-color="#4f46e5"/>
</radialGradient>
</defs>
<path id="blobPath" d="…"/>
</svg>
</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 |
|---|---|---|
| points = 8 | 8 | Number of radial vertices (JS constant). 5–6 = very round blob, 10–12 = more complex, detailed silhouette. |
| baseRadius = 30 | 30 | Rest radius in SVG units (100×100 viewBox). 20 = compact, 40 = large (watch for viewBox clipping). |
| data[i] * 15 | 15 | Maximum deformation amplitude in SVG units. 8 = subtle, 25 = highly expressive. |
| baseFreq = 0.3 | 0.3 | 3rd argument to getSimulatedAudio — base oscillation rate. 0.1 = slow and organic, 0.8 = fast and nervous. |
| * 0.2 (Catmull-Rom tension) | 0.2 | Bézier control-point tension factor. 0.1 = very smooth, 0.4 = more angular curves. |
| .blob-morph { width / height } | 120px | Rendering box size via CSS. The SVG scales proportionally — no JS change needed. |
| fill: url(#blobGradient) | indigo gradient | Blob color via SVG <radialGradient> in <defs>. Change stop-color for your palette. |
| filter: drop-shadow(…) | rgba(99,102,241,.5) 0 0 20px | Glow color and radius via CSS. Align the RGBA color with your base gradient for a coherent look. |
FAQ
getSimulatedAudio with an AnalyserNode read from the Web Audio API: analyser.getByteFrequencyData(dataArray), then normalize (dataArray[i] / 255). Pass this array in place of the simulated values — the Bézier machinery stays identical.getElementById('blobPath') targets a single element. For multiple instances, give each <path> a distinct id (blobPath2, blobPath3…) and refactor animateBlobMorph to accept the element as a parameter rather than using a global reference.let rafId; rafId = requestAnimationFrame(animate); and call cancelAnimationFrame(rafId) to stop. Alternatively, the if (!blobPath) return; guard halts the loop if you remove the #blobPath element from the DOM.