---
name: lava-lamp-background
description: Use when you want "Retro, playful, artistic" - Animates large organic blobs moving like a lava lamp.
---

# Lava Lamp Background

> **Category:** Background  -  **Personality:** Retro, playful, artistic
>
> **Best use:** Music, kids, creative sites
>
> **Ratings:** Professional 2/5 - Casual 5/5 - Exotic 5/5 - Visual impact 5/5 - Difficulty 4/5 - Performance cost 4/5

## What it is

A full-bleed background of large, warm "wax" blobs that slowly rise, swell, **merge and pinch apart** like the goo in a real lava lamp. The whole illusion comes from one trick: a layer of plain coloured circles is run through an **SVG gooey filter** (`feGaussianBlur` to soften every edge, then `feColorMatrix` to crank the alpha contrast back up). Where two softened blobs overlap their blurred halos cross the alpha threshold and weld into a single shape; as they drift apart the bridge stretches thin and snaps. It is unapologetically retro and playful, so it suits music, kids' and creative sites — and it is purely decorative, so it lives behind your real content.

> This is **distinct from "Animated Blobs"**: those stay separate and soft. Here the gooey filter makes neighbouring blobs physically fuse and split.

## Dependencies / CDN

**None — pure CSS + an inline SVG filter.** No libraries. The demo only loads display 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=Hanken+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&family=Unbounded:wght@600;700;800&display=swap" rel="stylesheet">
```

## HTML

The inline `<svg>` filter, a `.goo` layer of blobs (this is what the filter is applied to), and your content as a **sibling** so it never gets gooified:

```html
<div class="stage">
  <!-- the gooey filter lives anywhere in the document; referenced by id -->
  <svg class="defs" aria-hidden="true"><defs>
    <filter id="goo" x="-30%" y="-30%" width="160%" height="160%" color-interpolation-filters="sRGB">
      <feGaussianBlur in="SourceGraphic" stdDeviation="16" result="b"/>
      <feColorMatrix in="b" type="matrix"
        values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 15 -6"/>
    </filter>
  </defs></svg>

  <div class="goo" aria-hidden="true">
    <span class="blob b0 h-tang"></span><span class="blob b1 h-pink"></span>
    <span class="blob b2 h-gold"></span><span class="blob b3 h-tang"></span>
    <span class="blob b4 h-coral"></span><span class="blob b5 h-pink"></span>
    <span class="blob b6 h-gold"></span>
  </div>

  <div class="content"><!-- your real UI, z-index above the wax --></div>
</div>
```

## CSS

```css
.stage{position:relative; overflow:hidden; border-radius:26px; min-height:640px;
  isolation:isolate; background:linear-gradient(176deg,#1c0b36,#150827 46%,#0b0418)}
.defs{position:absolute; width:0; height:0; overflow:hidden}

/* THE EFFECT: blur softens edges, the filter region is enlarged so blobs
   don't clip as they drift past the box edges */
.goo{position:absolute; inset:-4%; z-index:0; filter:url(#goo)}

.blob{position:absolute; border-radius:50%; will-change:transform;
  animation:rise var(--dur,24s) ease-in-out var(--del,0s) infinite}

/* warm, ANALOGOUS hues only — opaque radials keep alpha=1 so merges read as
   molten in-between tones, never muddy (avoid complementary colours here) */
.h-tang {background:radial-gradient(circle at 38% 30%,#ffd89a,#ff7a3c 46%,#ff3f2c)}
.h-pink {background:radial-gradient(circle at 38% 30%,#ffb4cf,#ff3d8b 50%,#df1c6e)}
.h-gold {background:radial-gradient(circle at 38% 30%,#ffeaad,#ffb52e 52%,#ff871c)}

/* each blob: a low "rest" point (overlapping its neighbours = merged) and a
   high point near the top; different durations/delays stop them syncing up */
.b1{width:172px;height:172px;left:14%;top:68%;--dur:27s;--del:-3s;
  --down:42px;--up:-440px;--x0:0px;--x1:30px;--s0:.95;--s1:1.15}
/* …b0,b2–b6 follow the same pattern with their own sizes/tracks… */

@keyframes rise{
  0%  {transform:translate(var(--x0),var(--down)) scale(var(--s0))}
  50% {transform:translate(var(--x1),var(--up))   scale(var(--s1))}
 100% {transform:translate(var(--x0),var(--down)) scale(var(--s0))}
}

.content{position:relative; z-index:2}   /* sibling, NOT inside .goo */

/* reduced motion → freeze into a tasteful static arrangement */
@media (prefers-reduced-motion: reduce){ .blob{animation:none} }
```

The merge/split bridges happen for free: the filter re-rasterises `.goo` every frame, so any two overlapping blobs fuse and any separating pair tears apart.

## JavaScript

**Not required** — the effect is pure CSS/SVG. The demo adds only an optional pointer-parallax flourish, gated behind `prefers-reduced-motion`:

```js
var stage = document.querySelector('.stage'), goo = document.querySelector('.goo');
if(stage && goo && !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;
    var dy = ((e.clientY-r.top )/r.height - .5) * -18;
    goo.style.transform = 'translate3d('+dx.toFixed(1)+'px,'+dy.toFixed(1)+'px,0)';
  });
  stage.addEventListener('pointerleave', function(){ goo.style.transform=''; });
}
```

## How it works

- **`feGaussianBlur`** softens every blob into a fuzzy alpha gradient. On its own this just makes blurry circles.
- **`feColorMatrix`** rebuilds the alpha channel: the last row `0 0 0 15 -6` means `outAlpha = 15·inAlpha − 6`. Anything above ~0.4 alpha snaps to fully opaque, anything below snaps to transparent — so the blurred halos get a crisp edge again. Two halos that overlap clear the threshold **together**, producing one continuous shape with a smooth neck between them. That neck is the lava-lamp "merge".
- **`color-interpolation-filters="sRGB"`** keeps the colours vivid (the default `linearRGB` washes warm hues out).
- **Motion** is plain CSS `@keyframes` on each blob (`translate` + `scale`), with staggered durations/delays so the pattern never visibly loops. Overlapping low "rest" points are where blobs start merged; rising lifts them apart.

## Customization

- **Gooeyness / reach:** raise `stdDeviation` (bigger, softer merges from further apart) and/or the alpha multiplier in the matrix (`15` → sharper, more defined necks; lower → softer, more liquid).
- **Threshold:** the matrix offset (`-6`) sets where the edge falls; more negative = thinner blobs, less negative = fatter, more-fused mass.
- **Palette:** swap the blob radial-gradients. **Stay analogous** (all warm, or all cool) — complementary colours blend to grey/brown at the seams.
- **Tempo & density:** change each blob's `--dur`/`--del` and add/remove `.blob` spans. Fewer, larger blobs = calmer; more, smaller = busier.

## Accessibility & performance

- The whole `.goo` layer is re-blurred every frame — it is GPU-bound and the single most expensive thing here. Keep blob count modest (~6–8), animate **only `transform`** (never the blur radius or `width/height`), and add `will-change:transform`.
- It is **decorative**: mark the wrapper `aria-hidden="true"` and make sure foreground text has full contrast against it (a dark vessel background does this).
- **`prefers-reduced-motion`**: the keyframes are switched off and the blobs settle into a hand-placed static composition — verify that resting frame looks intentional.

## Gotchas

- **Enlarge the filter region.** A default filter region (`-10%…120%`) clips the blur and any blob that drifts past the box edge. Set `x="-30%" y="-30%" width="160%" height="160%"` on the `<filter>`.
- **Keep content a sibling of `.goo`,** never a child — anything inside the filtered layer gets gooified and blurred too.
- **Use opaque blobs.** The matrix thresholds *alpha*; semi-transparent fills or alpha gradients fight the threshold and the merge falls apart. Put colour variation in the RGB (radial gradients), not the alpha.
- **The stage needs `overflow:hidden` + `isolation:isolate`** so blobs clip to the vessel and the filter's stacking context behaves.
- **No filter support = sharp circles** (a graceful degrade — it simply looks like plain overlapping blobs, not lava).
- A parent `filter`/`transform` can create an unexpected containing block; keep `.goo` and `.content` as plain siblings inside `.stage`.
