Organic Blob Shapes in CSS: SVG Animations and clip-path Tricks
Build fluid organic blob shapes with CSS clip-path and SVG animations. Real code, no libraries needed — just modern CSS and a few SVG tricks developers actually use.
Why Blob Shapes Are Everywhere Right Now
Honestly, blob shapes are having a moment — and it's not hard to see why. Hard geometric layouts feel cold. Circles feel lazy. Blobs sit in that sweet spot between structure and chaos that makes a UI feel alive without trying too hard.
You'll find them in hero sections, avatar containers, decorative backgrounds, and loading indicators. They pair well with glassmorphism effects because the soft edges complement frosted glass panels. They also give claymorphism designs that inflated, bubbly quality that's impossible to fake with plain border-radius.
There are two main approaches: CSS clip-path with polygon() or path() values, and inline SVG with animated <path> elements. Both work. Each has tradeoffs. We'll cover both honestly.
Understanding clip-path: polygon() vs path()
The clip-path property clips an element to a shape. polygon() takes a list of x/y coordinate pairs — it's the simpler cousin. But polygons only produce straight-edged shapes. For actual blobs you need path(), which accepts a full SVG path d attribute string.
Browser support for clip-path: path() is solid as of mid-2026. Chrome 88+, Firefox 97+, Safari 14.1+. The edge case? It doesn't scale with the element automatically — the coordinates are absolute pixels. So a path() clipping a 400×400px div will break on a 200×200px container unless you restructure the path data. Keep that in mind.
For responsive blobs, the SVG approach is almost always better. For fixed-size decorative elements — hero blobs, background accents — clip-path: path() is surprisingly low-overhead.
Building a CSS-Only Morphing Blob
Here's a pure CSS blob that morphs between two shapes using @keyframes. No JavaScript. No library. Just a border-radius trick that most developers haven't fully explored yet.
The trick is that border-radius accepts up to eight values — four for horizontal radii and four for vertical radii, separated by a slash. Animating between two sets of eight values produces convincingly organic movement.
.blob {
width: 300px;
height: 300px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
animation: morph 8s ease-in-out infinite;
transition: all 1s ease-in-out;
}
@keyframes morph {
0% { border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; }
25% { border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%; }
50% { border-radius: 50% 60% 30% 60% / 40% 30% 70% 60%; }
75% { border-radius: 40% 60% 50% 40% / 60% 40% 30% 70%; }
100% { border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; }
}That 8-second duration feels natural. Too fast and it reads as a glitch. Too slow and nobody notices. Adjust to taste, but 6–10s is the range that actually works in practice.
SVG Path Animation for True Blob Morphing
The border-radius technique has limits. You can't get truly irregular shapes — the symmetry constraints of CSS border-radius always show through. For full organic control, you need SVG <path> animation using SMIL's <animate> element or the Web Animations API.
SMIL is technically deprecated in SVG 2.0 but still works in every browser in 2026, including Safari. For production code I'd use the Web Animations API or CSS @keyframes on a d property — which Chrome 93+ and Firefox 99+ support. Safari support for animating the d attribute via CSS landed in Safari 16. You're safe.
// BlobSVG.tsx — React component with animated SVG path
export function BlobSVG({ color = '#667eea' }: { color?: string }) {
return (
<svg
viewBox="0 0 200 200"
xmlns="http://www.w3.org/2000/svg"
className="w-64 h-64"
>
<path fill={color}>
<animate
attributeName="d"
dur="10s"
repeatCount="indefinite"
values="
M44.7,-54.2C56.5,-46.2,62.8,-29.4,66.4,-11.4C70.1,6.7,71,26,62.5,39.2C54,52.4,36.1,59.6,18.5,63.7C0.9,67.8,-16.5,68.9,-33.8,63.3C-51.1,57.8,-68.2,45.7,-73.9,29.2C-79.6,12.7,-73.8,-8.1,-64.3,-25.8C-54.7,-43.5,-41.3,-58.1,-26.3,-64.8C-11.4,-71.5,5.2,-70.3,20.9,-65.5C36.5,-60.7,51.2,-52.3,44.7,-54.2Z;
M39.5,-51.9C50.9,-40.9,59.5,-28.3,64.2,-13.2C68.9,1.9,69.7,19.4,63.5,33.7C57.2,47.9,44,59,29.2,65.2C14.4,71.3,-2,72.5,-18.2,68.6C-34.5,64.7,-50.6,55.7,-60.9,42.1C-71.2,28.6,-75.7,10.5,-72.7,-6.2C-69.7,-22.9,-59.2,-38.3,-46.1,-49.2C-33,-60.1,-17.3,-66.5,-1.3,-65.1C14.7,-63.7,28.2,-62.8,39.5,-51.9Z;
M44.7,-54.2C56.5,-46.2,62.8,-29.4,66.4,-11.4C70.1,6.7,71,26,62.5,39.2C54,52.4,36.1,59.6,18.5,63.7C0.9,67.8,-16.5,68.9,-33.8,63.3C-51.1,57.8,-68.2,45.7,-73.9,29.2C-79.6,12.7,-73.8,-8.1,-64.3,-25.8C-54.7,-43.5,-41.3,-58.1,-26.3,-64.8C-11.4,-71.5,5.2,-70.3,20.9,-65.5C36.5,-60.7,51.2,-52.3,44.7,-54.2Z
"
/>
</path>
</svg>
);
}The key requirement for <animate> morphing: both path strings must have the exact same number of commands and the same command types in the same order. Mismatched paths produce unpredictable interpolation or no animation at all. Tools like Blobmaker or Haikei export valid pairs, or you can write them by hand if you're patient.
Using clip-path: path() for Image Cropping
One underused application: cropping images into blob shapes. Normally you'd reach for SVG clipPath with a <use> element. But clip-path: path() on an <img> tag is cleaner markup.
The coordinate system is relative to the element's border box, starting at 0,0 in the top-left. For a 400×400px avatar image, something like this works well and gives you that organic edge without any extra DOM nodes.
.blob-avatar {
width: 400px;
height: 400px;
object-fit: cover;
clip-path: path(
'M200,20
C280,20 360,80 380,160
C400,240 370,330 300,370
C230,410 140,400 80,355
C20,310 10,220 30,145
C50,70 120,20 200,20Z'
);
}Combine this with a CSS filter: drop-shadow(0 8px 24px rgba(0,0,0,0.35)) on the parent container for depth. Note: box-shadow won't follow the clipped shape — drop-shadow is the filter that traces the actual visible pixels. That's a mistake I've seen in production more times than I care to admit.
Blob Backgrounds with Tailwind and Inline SVG
If you're on Tailwind v4.0.2, you can't directly express clip-path: path() values through utility classes — the arbitrary value syntax [clip-path:path(...)] works but gets messy fast for complex path strings. For blob backgrounds specifically, inline SVG elements positioned absolutely are the cleaner approach.
Here's how I typically implement decorative blob backgrounds in a React + Tailwind project. The blobs live in a fixed aria-hidden container so they don't affect layout or accessibility. If you're interested in how this compares to particle background effects, the short answer is SVG blobs are cheaper on the GPU — no canvas redraws.
// BackgroundBlobs.tsx
export function BackgroundBlobs() {
return (
<div
aria-hidden="true"
className="pointer-events-none fixed inset-0 overflow-hidden -z-10"
>
<svg
className="absolute -top-32 -left-32 w-[600px] h-[600px] opacity-30 blur-3xl"
viewBox="0 0 600 600"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="rgba(102,126,234,0.8)"
d="M300,60C420,60 520,140 540,260C560,380 490,490 380,530C270,570 140,530 80,420C20,310 40,160 130,100C200,52 240,60 300,60Z"
>
<animateTransform
attributeName="transform"
type="rotate"
from="0 300 300"
to="360 300 300"
dur="40s"
repeatCount="indefinite"
/>
</path>
</svg>
<svg
className="absolute -bottom-24 -right-24 w-[500px] h-[500px] opacity-20 blur-2xl"
viewBox="0 0 500 500"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="rgba(118,75,162,0.9)"
d="M250,50C360,50 440,130 460,240C480,350 430,450 330,475C230,500 110,460 60,360C10,260 40,130 130,80C180,55 210,50 250,50Z"
/>
</svg>
</div>
);
}The blur-3xl class (which maps to filter: blur(64px)) is doing a lot of work here. Without it, crisp blob edges look synthetic. The blur is what makes it read as ambient light, not clip art. Pair this with a theme toggle so your blobs adapt between light and dark — you'll want different opacity values: around 0.15 for light mode, 0.25–0.35 for dark.
Performance: What You're Actually Paying For
Animating blobs cheaply means staying on the compositor thread. Animations that mutate border-radius, clip-path, or SVG d attributes all trigger layout recalculation on most browsers — they're not compositor-only. The transform and opacity properties are the only guaranteed compositor-thread animations.
So what do you do? The practical answer is: use will-change: transform on blob elements you're animating, and prefer animateTransform (rotation, scale) over animate on d when possible. A slowly rotating or scaling blob using only transform will be smoother than a morphing blob recalculating path data 60 times per second.
For morphing blobs specifically, profile in Chrome DevTools before shipping. If you're seeing dropped frames, either reduce the animation duration (fewer interpolated steps per second), or move to a CSS @keyframes animation with will-change: clip-path — that lets the browser optimize more aggressively than SMIL. And honestly, at 8–12 second durations, most morphing blobs look fine even with the occasional dropped frame.
Generating Blob Paths Programmatically
Hand-writing SVG path data for blobs is miserable. There are better options. The most reliable is using a small utility function that generates smooth closed curves from a set of randomized points. Here's a minimal version that produces usable blob paths.
Why bother generating them in code? Because you can seed the randomization, store the seed, and reproduce the same blob every time. That's useful for user avatars, unique card decorations, or anything that needs to feel custom without being truly random on every render.
// generateBlobPath.js
// Produces a smooth SVG path string for a blob shape.
// cx, cy: center coords. radius: base size. variance: 0–1 irregularity.
function generateBlobPath(cx, cy, radius, variance = 0.4, points = 8) {
const angleStep = (Math.PI * 2) / points;
const coords = Array.from({ length: points }, (_, i) => {
const angle = i * angleStep - Math.PI / 2;
const r = radius * (1 - variance / 2 + Math.random() * variance);
return [
cx + r * Math.cos(angle),
cy + r * Math.sin(angle),
];
});
// Catmull-Rom to cubic Bezier conversion for smooth curves
const d = coords.map(([x, y], i) => {
const prev = coords[(i - 1 + points) % points];
const next = coords[(i + 1) % points];
const cp1x = x + (next[0] - prev[0]) / 6;
const cp1y = y + (next[1] - prev[1]) / 6;
const [nx, ny] = next;
const nextnext = coords[(i + 2) % points];
const cp2x = nx - (nextnext[0] - x) / 6;
const cp2y = ny - (nextnext[1] - y) / 6;
return `C ${cp1x},${cp1y} ${cp2x},${cp2y} ${nx},${ny}`;
});
return `M ${coords[0][0]},${coords[0][1]} ${d.join(' ')} Z`;
}
// Usage:
const path = generateBlobPath(200, 200, 150, 0.35, 8);
// Feed into <path d={path} />Is this production-ready? Mostly. The Catmull-Rom approximation gives smooth curves but doesn't guarantee perfect C1 continuity at all points. For decorative blobs nobody's measuring that. For precision artwork, use a proper spline library. For everything else on an actual project, this is fine and adds zero bundle weight.
FAQ
Yes, if you're using the border-radius 8-value trick you can animate between states entirely in CSS with @keyframes. For true SVG path morphing via CSS, you need to animate the d property — supported in Chrome 93+, Firefox 99+, and Safari 16+. Both paths must have identical command structures or the interpolation won't work.
Because box-shadow is rendered relative to the element's rectangular border box, before clipping is applied. Use filter: drop-shadow() on the parent element instead — it composites after clipping and follows the actual visible shape. Something like filter: drop-shadow(0 8px 24px rgba(0,0,0,0.35)) on the wrapper works well.
SMIL <animate> is declarative and lives inside the SVG markup — no JavaScript needed, and it degrades gracefully. CSS animation on the d property is more consistent with how you'd handle other CSS animations and integrates with DevTools timeline better. SMIL is technically deprecated in SVG 2.0 but still works in all browsers as of 2026. For new projects, CSS animation on d is the safer long-term bet.
No. The path coordinates are absolute pixels, so they don't scale with the element. If you need a blob clip-path that's responsive, either use SVG with a viewBox (which does scale), or recalculate the path data with a ResizeObserver and update a CSS custom property. The SVG route is much less painful.
It depends heavily on whether you're morphing paths or just transforming them. Morphing blobs (animating d or border-radius) can cause layout recalculation. I'd keep morphing blobs to 2–3 on screen simultaneously and test on a mid-range device. Pure transform animations (rotation, scale) can run more concurrently without issue — add will-change: transform to get the element onto its own compositor layer.
Use a seeded pseudo-random number generator instead of Math.random(). A simple mulberry32 PRNG is about 5 lines of code — feed it a seed string (like a user ID or product ID) and you'll get the same blob every time for that value. This is useful for user avatars or per-product decorations that need to feel unique but stay consistent across renders.