Blob Animation with CSS and SVG
Three ways to make blob animations: CSS border-radius, SVG paths, and SVG morphing, plus the gooey effect. Live demos, copy-paste code, and the performance details.
A blob is a soft, organic shape that slowly changes form. It shows up behind hero sections, as loading states, as cursor followers, and as the "alive" part of an otherwise still page.
There are three good ways to build one, and they suit different jobs:
- CSS
border-radius: one element, no JavaScript. Best for decorative shapes. - SVG paths: full control over the outline. Best when the blob has to react to something.
- SVG morphing: tween between shapes you design. Best for a fixed set of states.
Every demo on this page is live. Play with them, then copy the code.
1. The CSS blob: border-radius with eight values
Most people know border-radius: 50% makes a circle. Fewer know it takes eight values, split by a slash: four horizontal radii, then four vertical radii, each starting from the top-left corner and going clockwise.
.blob {
width: 240px;
aspect-ratio: 1;
background: #3d6bff;
/* horizontal radii: TL TR BR BL / vertical radii: TL TR BR BL */
border-radius: 42% 58% 70% 30% / 45% 45% 55% 55%;
}
Because each corner gets a different ellipse, the box stops looking like a box. Animate between a few of these and you have a blob:
.blob {
animation: morph 8s ease-in-out infinite;
}
@keyframes morph {
0%, 100% { border-radius: 42% 58% 70% 30% / 45% 45% 55% 55%; }
33% { border-radius: 70% 30% 46% 54% / 30% 29% 71% 70%; }
66% { border-radius: 30% 70% 70% 30% / 53% 30% 70% 47%; }
}
border-radius: 42% 58% 70% 30% / 45% 45% 55% 55%;The one rule that makes it look organic
Notice that the numbers come in pairs that add up to 100%: 42% 58%, 70% 30%, 45% … 55%. Each edge of the box is shared by two corners. When their radii add up to exactly 100%, the curves meet in the middle of that edge and the edge never goes flat.
So a good blob needs only four random numbers:
function randomBlob() {
const r = () => 25 + Math.round(Math.random() * 50); // 25%–75%
const [a, b, c, d] = [r(), r(), r(), r()];
return `${a}% ${100 - a}% ${c}% ${100 - c}% / ${b}% ${d}% ${100 - d}% ${100 - b}%`;
}
If the radii on an edge add up to more than 100%, the browser scales every radius down until they fit, which is why random eight-value blobs often come out lumpy or squashed.
Add rotation for extra life. A slow rotate on the same element hides the fact that the shape only has four "corners":
.blob {
animation: morph 8s ease-in-out infinite, spin 30s linear infinite;
}
@keyframes spin {
to { rotate: 360deg; }
}
When CSS blobs are the right choice: decorative backgrounds, avatars, image masks, and anywhere you want zero JavaScript. When they are not: a border-radius blob is always roughly an ellipse with four bulges. If you want more lobes, or a blob that reacts to the cursor or to audio, you need SVG.
2. The SVG blob: points on a circle, joined by a smooth curve
An SVG blob is a closed path. The recipe:
- Place
npoints evenly around a circle. - Push each point in or out a little.
- Draw one smooth curve through all of them.
Step 3 is the interesting part. A Catmull-Rom spline passes exactly through every point, and each segment can be written as an SVG cubic Bézier (C) command:
function smoothClosedPath(pts) {
const n = pts.length;
let d = `M${pts[0][0]},${pts[0][1]}`;
for (let i = 0; i < n; i++) {
const p0 = pts[(i - 1 + n) % n];
const p1 = pts[i];
const p2 = pts[(i + 1) % n];
const p3 = pts[(i + 2) % n];
const c1 = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
const c2 = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
d += ` C${c1[0]},${c1[1]} ${c2[0]},${c2[1]} ${p2[0]},${p2[1]}`;
}
return `${d}Z`;
}
function blobPoints(cx, cy, radius, factors) {
return factors.map((k, i) => {
const angle = (i / factors.length) * Math.PI * 2;
return [cx + Math.cos(angle) * radius * k, cy + Math.sin(angle) * radius * k];
});
}
factors is one number per point: 1 sits on the circle, 1.2 pushes 20% out, 0.8 pulls 20% in.
To animate it, give every point its own sine wave and rebuild the path each frame with requestAnimationFrame:
const path = document.querySelector("#blob");
const points = 7;
const wobble = 0.18;
function frame(now) {
const t = now / 1000;
const factors = Array.from({ length: points }, (_, i) => {
const speed = 0.6 + ((i * 0.618) % 1) * 0.8; // every point moves at its own pace
return 1 + wobble * Math.sin(t * speed + i * 2.399);
});
path.setAttribute("d", smoothClosedPath(blobPoints(160, 160, 100, factors)));
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
7 points → 7 cubic Bézier segments, redrawn every frameA few details that matter:
- Different speeds per point stop the blob from "breathing" in a visible rhythm. The
0.618and2.399(the golden ratio and the golden angle in radians) spread the speeds and phases so no two points line up. - Fewer points, softer blob. Five to eight points look organic. Twelve starts to look like a flower.
- Keep wobble under about 30%. Past that, neighbouring points can cross and the curve folds over itself.
- Update the attribute, not React state. Writing
dstraight onto the element skips a re-render every frame.
Because you own every point, this blob can react to anything: map the cursor distance to wobble, or an audio level from the Web Audio API to radius.
3. SVG morphing: tween between shapes you choose
Sometimes you do not want endless random motion. You want shape A to become shape B. SVG can interpolate a path's d attribute directly, with one strict condition:
Both paths must have the same number of commands, of the same types, in the same order.
M + 8 C + Z can morph into another M + 8 C + Z. It cannot morph into M + 6 C + Z, and it cannot morph into a path drawn with L and A commands. When the structure does not match, the browser jumps between shapes instead of animating.
That is exactly why the generator above is useful: every blob built with the same number of points has an identical command list, so any two of them morph cleanly.
The most compatible way to animate it is SMIL, the animation syntax built into SVG:
<svg viewBox="0 0 320 320">
<path fill="#3d6bff" d="M… (shape A)">
<animate
attributeName="d"
dur="6s"
repeatCount="indefinite"
values="M… (A); M… (B); M… (C); M… (A)"
keyTimes="0; 0.33; 0.66; 1"
calcMode="spline"
keySplines="0.45 0 0.55 1; 0.45 0 0.55 1; 0.45 0 0.55 1"
/>
</path>
</svg>
calcMode="spline" with keySplines gives each leg of the morph an ease-in-out curve instead of a robotic linear tween.
3 shapes · M + 8 C + Z each · same commands, so the browser can tween themCan you morph SVG paths with CSS instead?
Partly. The CSS d property accepts d: path("M…") and can be transitioned:
.blob path {
d: path("M… (shape A)");
transition: d 0.8s ease-in-out;
}
.blob:hover path {
d: path("M… (shape B)");
}
It works in Chromium and Firefox, but MDN lists it as limited availability because Safari's support has been incomplete. Treat it as a progressive enhancement and use SMIL or JavaScript when the morph has to work everywhere.
Morphing between shapes with different structures (a star into a circle, a logo into an icon) needs a library that adds and matches points for you, such as flubber or GSAP's MorphSVG plugin.
Bonus: the gooey effect
The "gooey" or metaball effect makes separate circles melt together when they get close. It is an SVG filter with two steps: blur everything, then crank the alpha contrast so the soft edges snap back to a hard edge. Where two blurs overlap, their alpha adds up and the shapes fuse.
<svg>
<defs>
<filter id="goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="12" result="blur" />
<feColorMatrix in="blur" mode="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 22 -9" result="goo" />
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
</filter>
</defs>
<g filter="url(#goo)">
<circle cx="200" cy="150" r="46" />
<circle cx="260" cy="150" r="38" />
</g>
</svg>
The last row of the matrix, 0 0 0 22 -9, is the whole trick: it multiplies alpha by 22 and subtracts 9, so anything less than about 40% opaque disappears and anything above it becomes solid. Raise stdDeviation for a stickier, stretchier goo.
Toggle the filter off to see what is really happening: four plain circles.
Performance
Blobs are cheap if you keep them in proportion:
- CSS blobs repaint every frame. Animating
border-radiusdoes not trigger layout, but it does trigger paint. One or two decorative blobs are fine; dozens of large ones are not. Animatingtransform(rotate, scale) on top is nearly free. - SVG path updates are cheap for a handful of paths. Rebuilding a seven-point path 60 times a second is trivial work for the CPU.
- Filters are the expensive part. A large
feGaussianBluris the costliest thing on this page. Keep gooey effects small, and never put a blur filter on a full-screen element. - Stop when nobody is looking. Pause
requestAnimationFrameloops when the blob scrolls out of view with anIntersectionObserver. Every demo here does.
Accessibility
A blob is decoration, so treat it that way:
- Mark it
aria-hidden="true"so screen readers skip it. - Respect reduced motion. Some people get dizzy from constant movement:
@media (prefers-reduced-motion: reduce) {
.blob {
animation: none;
}
}
For JavaScript blobs, check window.matchMedia("(prefers-reduced-motion: reduce)") and draw a single still frame instead of starting the loop.
Which one should you use?
| You want… | Use |
|---|---|
| A decorative background shape, no JavaScript | CSS border-radius |
| A blob that reacts to cursor, scroll or sound | SVG path + requestAnimationFrame |
| Smooth transitions between a few designed shapes | SVG morph with SMIL |
| Shapes that melt into each other | SVG gooey filter |
FAQ
How do I make a blob shape with CSS?
Give an element a background and an eight-value border-radius such as 42% 58% 70% 30% / 45% 45% 55% 55%. Keep the two radii on each edge adding up to 100% so the edges stay curved.
How do I animate a blob in CSS?
Put three or four eight-value border-radius shapes in a @keyframes rule and run it with animation: morph 8s ease-in-out infinite. Add a slow rotate for more organic motion.
Why does my SVG morph jump instead of animate?
The two paths have different structures. Morphing only works when both d values contain the same number and type of commands in the same order. Generate both shapes from the same number of points, or use a morphing library.
Do blob animations hurt performance?
Rarely. A few blobs cost very little. Problems come from many large border-radius blobs repainting every frame, or from big blur filters. Pause off-screen animations and keep filters small.