CSS Morphing Blob

An organic shape that continuously morphs using animated border-radius keyframes.

Published June 27, 2026 Intermediate 8 min read

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.

Three blobs, one keyframe

CSS
blob

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%; }
}

Other ways to build it

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.

drift

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.

AC
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%;
  }
}

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. Each blob uses a negative animation-delay to desync the cycles so they read as independent shapes.

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

CSS properties used

border-radius
In its eight value form, four horizontal radii then four vertical radii separated by /. Animating all eight is what produces the morph.
animation-duration
Different durations on each blob keep the group from locking into one rhythm. Durations that are not multiples of one another never realign.
animation-delay
Negative values start the animation partway through its cycle immediately. Positive values hold the first keyframe, which shows as a pause after load.
animation-timing-function
ease-in-out applies to each segment between keyframes, not to the animation overall, so the shape settles briefly at all four stops.
background
A radial-gradient with an off center focal point reads as a light source, which is what keeps a flat color blob from looking like a sticker.
overflow
hidden on the blob makes the animated radius clip its children, which is how the framed variant below works.

Browser support

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

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.

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.

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.

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, 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 maths. 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 genuinely 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.