---
name: animated-blobs
description: Use when you want "Friendly, playful, modern" - Moves rounded abstract shapes slowly behind content.
---

# Animated Blobs

> **Category:** Background  -  **Personality:** Friendly, playful, modern
>
> **Best use:** Landing pages, education, kids
>
> **Ratings:** Professional 4/5 - Casual 5/5 - Exotic 3/5 - Visual impact 4/5 - Difficulty 2/5 - Performance cost 3/5

## What it is

Animated Blobs scatters a few big, soft, blurred gradient shapes behind your content and lets each one slowly **drift** while its **`border-radius` morphs** between organic values, so the background feels alive and hand-made instead of flat. Because the blobs are heavily blurred they read as gentle clouds of colour — friendly, playful and modern, which is why it suits landing pages, education and kids' products. Crucially the blobs stay **soft and separate**: unlike a lava-lamp / gooey effect there is no SVG "goo" filter merging them. It is pure CSS, cheap to ship, and degrades to a calm static gradient for reduced-motion users.

## Dependencies / CDN

**None - pure CSS.** The morph + drift are CSS keyframes; the optional pointer parallax is a few lines of vanilla JS. The demo only loads two display/body fonts (optional):

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@500;600;700&family=Quicksand:wght@500;600;700&display=swap" rel="stylesheet">
```

## HTML

A blurred blob layer behind a content layer — that's the whole structure:

```html
<div class="stage">
  <div class="field" aria-hidden="true">
    <span class="blob b1"></span><span class="blob b2"></span><span class="blob b3"></span>
  </div>
  <div class="content">
    <h1>Where curious kids <em>bloom</em>.</h1>
    <p>Ten playful minutes a day.</p>
  </div>
</div>
```

## CSS

```css
.stage{position:relative; overflow:hidden; border-radius:30px; min-height:560px; isolation:isolate;
  display:flex; align-items:center; justify-content:center;
  background:linear-gradient(158deg,#fff6ec,#fdeef6 52%,#f1edff)}

.field{position:absolute; inset:-8%; z-index:0}          /* the blob layer, slightly overscanned */
.blob{position:absolute; filter:blur(54px); opacity:.8; will-change:transform,border-radius;
  /* organic radius: 4 horizontal %  /  4 vertical % */
  border-radius:42% 58% 62% 38% / 48% 42% 58% 52%}

.b1{width:46%; height:55%; top:-8%; left:-6%;  background:linear-gradient(135deg,#ffd6a5,#ff8e72);
  animation:morph 17s ease-in-out infinite,        drift 23s ease-in-out infinite}
.b2{width:44%; height:52%; top:-5%; right:-8%; background:linear-gradient(135deg,#ffe98a,#ffbf3f);
  animation:morph 21s ease-in-out infinite reverse, drift 27s ease-in-out infinite reverse}
/* ...add 2-3 more, varying size / position / colour / duration so they de-sync... */

/* 1) SHAPE: morph ONLY border-radius */
@keyframes morph{
  0%,100%{border-radius:42% 58% 62% 38% / 48% 42% 58% 52%}
  34%    {border-radius:62% 38% 44% 56% / 56% 58% 42% 44%}
  67%    {border-radius:38% 62% 56% 44% / 42% 54% 46% 58%}
}
/* 2) MOVEMENT: change ONLY transform — keep it off the morph so the two never clash */
@keyframes drift{
  0%,100%{transform:translate3d(0,0,0)    rotate(0deg)  scale(1)}
  50%    {transform:translate3d(34px,26px,0) rotate(14deg) scale(1.1)}
}

.content{position:relative; z-index:1}                    /* content sits above the blobs */

@media (prefers-reduced-motion: reduce){ .blob{animation:none} }  /* soft + static */
```

## JavaScript

Not required. Optional flourish — parallax the whole blob layer a few px relative to the cursor (the CSS keeps morphing/drifting on its own). Gate it behind `prefers-reduced-motion`:

```js
var stage=document.querySelector('.stage'), field=document.querySelector('.field');
if(stage && field && !matchMedia('(prefers-reduced-motion: reduce)').matches){
  stage.addEventListener('pointermove',function(e){
    var r=stage.getBoundingClientRect();
    var dx=((e.clientX-r.left)/r.width -.5)*-26, dy=((e.clientY-r.top)/r.height-.5)*-26;
    field.style.transform='translate3d('+dx+'px,'+dy+'px,0)';
  });
  stage.addEventListener('pointerleave',function(){ field.style.transform=''; });
}
```

## How it works

- **The organic shape.** `border-radius` accepts eight values — four *horizontal* radii before the slash, four *vertical* radii after it. Asymmetric percentages bend a square into a lopsided oval; morphing between several such sets makes the blob gently "breathe" and reshape.
- **Two animations per blob, on purpose.** One keyframe set drives `border-radius` (shape), the other drives `transform` (translate / rotate / scale = position + size). They must be **separate** because two animations can't both write `transform` — if the morph also set a transform, only one would win and the other would vanish.
- **Blur is what sells it.** `filter:blur(54px)` dissolves the hard edge into a soft cloud; `opacity` lets overlapping blobs tint each other *without* truly merging — they stay distinct shapes.
- **De-sync everything.** Different sizes, positions, colours and mismatched durations (with `reverse` on some) stop the loop ever looking repetitive or mechanical.
- **Layering.** The `.content` layer at `z-index:1` sits above the `z-index:0` `.field`; `isolation:isolate` + `overflow:hidden` keep the whole thing clipped inside the rounded stage.

## Customization

- **Density:** 3 blobs = calm; 5-6 = lively. Keep the count small (blur is the costly part).
- **Palette:** swap the gradient stops. Pastels on a light background read friendly/education; neons on near-black read nightlife/club.
- **Energy:** larger `translate`/`scale` in `drift` + shorter durations = playful; smaller moves + 25-40s durations = serene/corporate.
- **Softness:** more blur = dreamier; less blur lets the morphing edge actually show.
- **Legibility pad:** drop a faint `radial-gradient` halo behind the headline so text stays crisp wherever a blob drifts (the demo does this with `.ab-hero::before`).

## Accessibility & performance

- **Honour `prefers-reduced-motion`:** set `animation:none` so blobs hold a pleasant *static* organic shape (the base `border-radius` is that resting frame).
- **Repaint cost:** animating `border-radius` is **not** GPU-composited the way `transform`/`opacity` are, so it repaints each frame — keep the blob count and stage size sensible; `will-change` hints the compositor. The drift (transform/scale) stays cheap.
- **Contrast:** blobs are decorative (`aria-hidden="true"`); make sure body copy keeps contrast over the lightest/darkest spot a blob can reach — the halo helps.
- **No layout impact:** blobs are absolutely positioned inside an `overflow:hidden` parent, so they never create scrollbars. Never size them with `100vw`.

## Gotchas

- **Don't combine `border-radius` and `transform` in one `@keyframes`** if you also animate transform separately — split them into a `morph` animation and a `drift` animation (this demo runs exactly one of each per blob).
- **This is NOT a lava lamp.** There's no `feGaussianBlur` + `feColorMatrix` "goo" filter, so blobs never visibly merge or split — they stay soft and separate. If you *want* merging, reach for the Lava-Lamp technique instead.
- **The parent needs `overflow:hidden`** (plus a `border-radius`) and ideally `isolation:isolate`, or a blurred blob bleeds outside the card / a negative `z-index` halo escapes the stacking context.
- **Blur is the bottleneck:** very large blur radii on many big blobs can drop frames on low-end devices — a few large blobs beat many small ones.
- **Keep blobs decorative:** never place real content inside a `.blob`; it's `aria-hidden` and constantly moving.
