Most demos need a newer browser

They use :has() and container queries. Update your browser, or on an iPhone update iOS to 16.4 or later.

CSS Morphing Blob Animation

An organic shape that continuously morphs using animated border-radius keyframes. The eight-value syntax gives each corner two radii.

Level
Intermediate
Published
Updated
Reading time
8 min

Try it

3 live demos

CSS
blob

Three blobs, one keyframe

Other ways to build it

Build it

6 snippets to copy

CSS Morphing Blob Animation

HTML

<div class="blob-stage">
  <div class="blob blob-green">
    <span class="blob-label">CSS</span>
  </div>
  <div class="blob blob-blue">
    <span class="blob-label">blob</span>
  </div>
  <div class="blob blob-pink"></div>
</div>

CSS

.blob-stage {
  display: flex;
  gap: 2rem;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  padding: 1rem;
}

.blob {
  display: flex;
  align-items: center;
  justify-content: center;
  animation: blob-morph 8s ease-in-out infinite;
}

.blob-green {
  width: 120px;
  height: 120px;
  background: radial-gradient(circle at 30% 30%, #b8ff57, #6ddf00);
}

/* negative delays desync the cycles so the blobs look independent */
.blob-blue {
  width: 100px;
  height: 100px;
  background: radial-gradient(circle at 30% 30%, #38bdf8, #c084fc);
  animation-delay: -3s;
  animation-duration: 6s;
}

.blob-pink {
  width: 80px;
  height: 80px;
  background: radial-gradient(circle at 30% 30%, #f472b6, #ff9040);
  animation-delay: -1.5s;
  animation-duration: 7s;
}

.blob-label {
  color: #0c0c0d;
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-align: center;
}

/* four horizontal radii / four vertical radii, animated */
@keyframes blob-morph {
  0%, 100% { 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% 50% 40% / 40% 30% 60% 50%; }
  75%      { border-radius: 40% 50% 60% 30% / 60% 40% 50% 70%; }
}

Morph and drift at the same time

A single element can run more than one animation. Comma separate them in the animation shorthand and each gets its own duration and timing function, which is how a blob can breathe on an eight second cycle while it wanders on an eleven second one. Two cycles that do not divide into each other never repeat the same combination, so the motion stays unpredictable from a fixed pair of keyframes. Keep the shape work in one animation and the position work in the other: the drift is a transform, so it costs no paint, while the morph repaints the element every frame.

HTML

<div class="blob-stage">
  <div>
    <div class="blob blob-drifter blob-drift-a"><span class="blob-label">drift</span></div>
  </div>
  <div>
    <div class="blob blob-drifter blob-drift-b"></div>
  </div>
</div>

CSS

/* uses @keyframes blob-morph from the main example above; the rules here are
   what changes, not the whole file */
/* two animations, two rhythms, one element */
.blob-drifter {
  width: 120px;
  height: 120px;
  background: radial-gradient(circle at 30% 30%, #b8ff57, #6ddf00);
  animation:
    blob-morph 8s ease-in-out infinite,
    blob-drift 11s ease-in-out infinite;
}

.blob-drift-b {
  width: 92px;
  height: 92px;
  background: radial-gradient(circle at 30% 30%, #c084fc, #38bdf8);
  animation:
    blob-morph 6s ease-in-out -2s infinite,
    blob-drift 9s ease-in-out -4s infinite;
}

/* transform only, so the wander costs no paint */
@keyframes blob-drift {
  0%, 100% { transform: translate(0, 0) rotate(0deg); }
  33%      { transform: translate(14px, -10px) rotate(8deg); }
  66%      { transform: translate(-10px, 8px) rotate(-6deg); }
}

@media (prefers-reduced-motion: reduce) {
  .blob-drifter {
    animation: none;
    border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
  }
}

A blob used as a content frame

Rounding a box does not clip what is inside it. Add overflow: hidden and the animated radius becomes a mask, so a photograph, an initial or an icon is cut to the moving outline. The label here is a child of the blob rather than a sibling, which means it inherits the clip: push it near the edge and the corners trim it. Center the content and leave a margin of roughly a fifth of the box on every side. This is the version worth using for an avatar, because the image stays a plain rectangle in the markup and all the shaping happens in CSS.

HTML

<div class="blob-frame">
  <img src="/avatar.jpg" alt="" class="blob-frame-art">
  <span class="blob-frame-label">AC</span>
</div>

CSS

/* uses @keyframes blob-morph from the main example above; the rules here are
   what changes, not the whole file */
.blob-frame {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 130px;
  height: 130px;
  /* without this the child ignores the curve entirely */
  overflow: hidden;
  animation: blob-morph 9s ease-in-out infinite;
}

.blob-frame-art {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(45deg,
      rgba(12, 12, 13, .18) 0 8px,
      transparent 8px 16px),
    radial-gradient(circle at 30% 25%, #38bdf8, #c084fc);
}

.blob-frame-label {
  position: relative;
  color: #0c0c0d;
  font-size: 1.1rem;
  font-weight: 700;
  letter-spacing: .06em;
}

@media (prefers-reduced-motion: reduce) {
  .blob-frame {
    animation: none;
    border-radius: 50% 60% 50% 40% / 40% 30% 60% 50%;
  }
}

Understand it

How it works and 6 properties

A CSS morphing blob is a square box whose border-radius is animated between several lopsided values, so the outline flexes instead of holding a fixed curve. The eight value form of border-radius gives every corner an independent horizontal and vertical radius, which is enough control to draw a shape that does not read as a rounded rectangle at all. No SVG path, no canvas, no script.

All three blobs below run the same four keyframes. Only size, gradient and timing differ, and that is enough to stop them looking like copies of each other. Two more builds follow: a blob that drifts while it morphs by stacking a second animation on the same element, and a blob used as a clipping frame for real content.

How it works

CSS border-radius accepts up to 8 values: four horizontal radii, then four vertical radii after a /. Animating between combinations such as 60% 40% 30% 70% / 60% 30% 70% 40% and 30% 60% 70% 40% / 50% 60% 30% 60% produces a continuous organic morph.

The long form of border-radius is two groups of four values separated by a slash. The first group is the horizontal radius of the top left, top right, bottom right and bottom left corners. The second group is the vertical radius of the same four corners in the same order. When the two numbers for a corner differ, that corner is a quarter of an ellipse rather than a quarter of a circle, and a shape built from four mismatched ellipse quadrants is what stops the outline reading as a button.

Percentages resolve against the box, horizontal radii against its width and vertical radii against its height. A square box therefore keeps the same shape at 80px or 800px, which is why the demo sizes every blob as a square. Put the same values on a 300x120 box and the corners stretch with it, and the result looks squashed rather than organic. If the box has to be a non-square, express the radii in percentages of the shorter side by hand or accept the distortion as part of the design.

All four keyframes supply all eight values in the same order and the same unit, which is what lets the browser interpolate corner by corner. Leave a value out of one keyframe and the shorthand fills it in from the standard shorthand rules, not from the neighboring keyframe, so the shape jumps at that stop. The timing function is ease-in-out, and because a timing function applies to every keyframe segment separately rather than to the animation as a whole, the blob eases into and out of each of the four stops. That is what gives it a breathing rhythm instead of a constant churn.

The three blobs stay out of phase for two reasons. Each has a different duration (8s, 6s and 7s), and two of them start partway through their cycle with a negative animation-delay. A negative delay is not a wait: it runs the animation immediately as though it had already been playing for that long, so -3s on a 6s cycle starts the blob at its halfway point. A positive delay would instead freeze the blob at its first keyframe for three seconds after load, which is very visible. The same trick drives the staggering on CSS particle float.

border-radius is a paint property, not a composited one. Changing it does not force layout, so the surrounding page never reflows, but the browser does repaint the element on every frame. Three modest blobs cost almost nothing. Forty full bleed ones behind a hero section will show up as paint time in a performance trace. When the movement matters more than the outline, animate transform instead and let the radius sit still, because transforms run on the compositor and cost no paint at all.

Accessibility notes

A blob is almost always decoration. Give the element aria-hidden="true" when it carries no meaning, or leave it as an empty div with no text, which screen readers skip anyway. The framed variant below is the exception: it holds real content, so the wrapper stays in the accessibility tree and only the shape is decorative.

Continuous ambient motion in the corner of the eye pulls attention away from the text a reader is trying to get through, and for someone with a vestibular condition slow organic movement is more provoking than a quick transition, not less, because it never ends. Wrap the animation in prefers-reduced-motion and let the blob settle on one of its four shapes. The variants below do exactly that.

Text placed on a blob sits over a gradient, so contrast changes across the shape. Check the ratio against the darkest and the lightest part of the gradient rather than against its average, and keep the label near the center where the color is most stable. The label in the demo is near black on a bright accent, which clears the ratio at both ends.

Mistakes worth avoiding

  • Using a non-square box. Horizontal radii resolve against width and vertical radii against height, so the same eight values that look organic on a square look stretched on a wide box.
  • Reaching for a positive animation-delay to desync a group. That holds each element on its first keyframe for the length of the delay, so the group sits frozen after load and then starts one by one. Negative delays start mid cycle instead, with nothing visible at the join.
  • Giving every blob the same duration. Identical cycles stay locked together forever no matter what the delays are, and a group that moves as one unit reads as a single animated image.
  • Expecting the curve to clip content. border-radius only rounds the box's own background and border. Children spill past the curve until you set overflow: hidden on the blob.
  • Animating a full screen blob. The element repaints every frame, and at viewport size that paint is expensive enough to drop frames on a mid range phone. Scale down the painted element and use transform: scale() to enlarge it instead.

Ship it

4 features in 4 browsers, 5 questions

Browser support

Feature Chrome Firefox Safari Edge
border-radius433.112
animation455.112
transform43.53.112
prefers-reduced-motion746310.179

Figures come from Can I Use. There is nothing new here, so the technique works everywhere without a fallback or a prefix. The one thing worth checking on older mobile Safari is a blob with overflow: hidden over a transform: some builds clip the child against the unrounded box for a frame before settling. Adding isolation: isolate on the blob is the usual workaround.

What you can build with it

  • Hero backdrops. Two or three large blurred blobs behind a headline, which gives a flat page depth without an image request.
  • Avatar frames. A morphing shape with overflow: hidden around a photograph, the pattern in the second variant below.
  • Section dividers. A single wide blob between two sections, softening the join without an SVG wave.
  • Loading indicators. A small blob that morphs while a request is in flight reads as activity without spinning, which suits a slow operation better than a spinner.
  • Badge and tag shapes. A static value from the same keyframe set, used once and never animated, gives a hand drawn outline for a category chip.

Frequently asked questions

How do you make a blob shape in pure CSS?
Give a square element eight border-radius values, four horizontal and four vertical separated by a slash, and make them uneven. 60% 40% 30% 70% / 60% 30% 70% 40% is a good starting point. Animating between several of those value sets is what turns a static blob into a morphing one.
Why does my blob look squashed?
The box is not square. Percentage radii resolve against width horizontally and height vertically, so a wide box stretches every corner along with it. Set an explicit square size, or use aspect-ratio: 1.
Does border-radius animate smoothly?
Yes, border-radius animates smoothly, as long as every keyframe lists the same number of values in the same unit. The browser interpolates each of the eight radii independently. Omitting values in one keyframe makes the shorthand expand differently there, and the shape snaps at that stop.
Is a CSS blob better than an animated SVG path?
For a simple four corner shape it is smaller and needs no path math. An SVG path with d interpolation can produce shapes a rounded box cannot, such as concave curves or more than four control points. If the blob needs to look irregular rather than softly uneven, SVG is the right tool.
How do I stop several blobs animating in sync?
Give them durations that are not multiples of each other, then start each one partway through with a negative animation-delay. Two blobs at 6s and 7s drift apart continuously; two at 6s and 12s realign every 12 seconds.