16 CSS Gradient Animations 13 / 16

CSS Liquid Lava Lamp Blob Animation

Three themed lava lamps — Ember (orange-pink), Aether (cyan-violet), and Growth (green-yellow) — each containing morphing radial-gradient blobs that simultaneously change shape via border-radius keyframes and float vertically, simulating fluid droplets in a heated lamp.

CSS + JS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="ga-13">
  <div class="ga-13__lamp ga-13__lamp--a">
    <div class="ga-13__blob"></div>
    <div class="ga-13__blob"></div>
    <div class="ga-13__blob"></div>
    <span class="ga-13__lamp-label">Ember</span>
  </div>
  <div class="ga-13__lamp ga-13__lamp--b">
    <div class="ga-13__blob"></div>
    <div class="ga-13__blob"></div>
    <div class="ga-13__blob"></div>
    <span class="ga-13__lamp-label">Aether</span>
  </div>
  <div class="ga-13__lamp ga-13__lamp--c">
    <div class="ga-13__blob"></div>
    <div class="ga-13__blob"></div>
    <div class="ga-13__blob"></div>
    <span class="ga-13__lamp-label">Growth</span>
  </div>

  <div class="ga-13__ctrl">
    <button class="ga-13__ctrl-btn" data-dur="18s">Slow</button>
    <button class="ga-13__ctrl-btn active" data-dur="8s">Normal</button>
    <button class="ga-13__ctrl-btn" data-dur="3s">Fast</button>
  </div>
</div>
.ga-13, .ga-13 *, .ga-13 *::before, .ga-13 *::after {
  margin: 0; padding: 0; box-sizing: border-box;
}
.ga-13 ::selection { background: rgba(249,115,22,.4); color: #fff; }

.ga-13 {
  --bg: #0a0510;
  --dur: 8s;
  width: 100%;
  min-height: 100vh;
  background: var(--bg);
  font-family: system-ui, -apple-system, sans-serif;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 32px;
  flex-wrap: wrap;
  padding: 48px 24px;
  position: relative;
}

/* ── Single lava blob ── */
.ga-13__lamp {
  position: relative;
  width: 180px;
  height: 260px;
  border-radius: 16px;
  overflow: hidden;
  background: #100820;
  border: 1px solid rgba(255,255,255,.06);
  flex-shrink: 0;
}

/* Container label */
.ga-13__lamp-label {
  position: absolute;
  bottom: 14px;
  left: 0; right: 0;
  text-align: center;
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .09em;
  text-transform: uppercase;
  z-index: 3;
}

/* Each blob is a div with morphing border-radius + position animation */
.ga-13__blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(2px);
}

/* ── Lamp A: Orange-pink ── */
.ga-13__lamp--a { background: #0d060a; }
.ga-13__lamp--a .ga-13__blob:nth-child(1) {
  width: 100px; height: 100px;
  background: radial-gradient(circle at 35% 40%, #ff6b2b, #ff2d7f);
  animation: ga-13-morph-a1 var(--dur) ease-in-out infinite, ga-13-float-a1 var(--dur) ease-in-out infinite;
  top: 60%; left: 40px;
}
.ga-13__lamp--a .ga-13__blob:nth-child(2) {
  width: 70px; height: 70px;
  background: radial-gradient(circle at 40% 35%, #ffab40, #ff5252);
  animation: ga-13-morph-a2 calc(var(--dur) * 1.3) ease-in-out infinite, ga-13-float-a2 calc(var(--dur) * 1.3) ease-in-out infinite;
  top: 20%; left: 55px;
  animation-delay: calc(var(--dur) * -.4);
}
.ga-13__lamp--a .ga-13__blob:nth-child(3) {
  width: 50px; height: 50px;
  background: radial-gradient(circle, #ff9d00, #ff3d00);
  animation: ga-13-morph-a2 calc(var(--dur) * .9) ease-in-out infinite, ga-13-float-a3 calc(var(--dur) * .9) ease-in-out infinite;
  top: 40%; left: 70px;
  animation-delay: calc(var(--dur) * -.65);
}
.ga-13__lamp--a .ga-13__lamp-label { color: #ff7b3a; }

/* ── Lamp B: Cyan-violet ── */
.ga-13__lamp--b { background: #050a12; }
.ga-13__lamp--b .ga-13__blob:nth-child(1) {
  width: 110px; height: 90px;
  background: radial-gradient(circle at 40% 45%, #00e5ff, #6c63ff);
  animation: ga-13-morph-b1 calc(var(--dur) * 1.1) ease-in-out infinite, ga-13-float-b1 calc(var(--dur) * 1.1) ease-in-out infinite;
  top: 55%; left: 35px;
}
.ga-13__lamp--b .ga-13__blob:nth-child(2) {
  width: 75px; height: 85px;
  background: radial-gradient(circle at 45% 35%, #18ffff, #7c4dff);
  animation: ga-13-morph-b2 calc(var(--dur) * .85) ease-in-out infinite, ga-13-float-b2 calc(var(--dur) * .85) ease-in-out infinite;
  top: 15%; left: 50px;
  animation-delay: calc(var(--dur) * -.3);
}
.ga-13__lamp--b .ga-13__blob:nth-child(3) {
  width: 45px; height: 55px;
  background: radial-gradient(circle, #64ffda, #536dfe);
  animation: ga-13-morph-b1 calc(var(--dur) * 1.4) ease-in-out infinite, ga-13-float-a3 calc(var(--dur) * 1.4) ease-in-out infinite;
  top: 35%; left: 60px;
  animation-delay: calc(var(--dur) * -.55);
}
.ga-13__lamp--b .ga-13__lamp-label { color: #00e5ff; }

/* ── Lamp C: Green-yellow ── */
.ga-13__lamp--c { background: #030d07; }
.ga-13__lamp--c .ga-13__blob:nth-child(1) {
  width: 95px; height: 105px;
  background: radial-gradient(circle at 38% 42%, #00e676, #ffea00);
  animation: ga-13-morph-c1 calc(var(--dur) * .95) ease-in-out infinite, ga-13-float-c1 calc(var(--dur) * .95) ease-in-out infinite;
  top: 58%; left: 42px;
}
.ga-13__lamp--c .ga-13__blob:nth-child(2) {
  width: 65px; height: 75px;
  background: radial-gradient(circle at 42% 38%, #b2ff59, #69f0ae);
  animation: ga-13-morph-c2 calc(var(--dur) * 1.2) ease-in-out infinite, ga-13-float-c2 calc(var(--dur) * 1.2) ease-in-out infinite;
  top: 18%; left: 58px;
  animation-delay: calc(var(--dur) * -.45);
}
.ga-13__lamp--c .ga-13__blob:nth-child(3) {
  width: 42px; height: 48px;
  background: radial-gradient(circle, #ccff90, #00c853);
  animation: ga-13-morph-c1 calc(var(--dur) * 1.5) ease-in-out infinite, ga-13-float-a1 calc(var(--dur) * 1.5) ease-in-out infinite;
  top: 38%; left: 68px;
  animation-delay: calc(var(--dur) * -.7);
}
.ga-13__lamp--c .ga-13__lamp-label { color: #69f0ae; }

/* Morph keyframes (border-radius) */
@keyframes ga-13-morph-a1 {
  0%   { border-radius: 60% 40% 70% 30% / 50% 60% 40% 50%; }
  25%  { border-radius: 30% 70% 40% 60% / 60% 40% 70% 30%; }
  50%  { border-radius: 50% 50% 30% 70% / 40% 60% 50% 60%; }
  75%  { border-radius: 70% 30% 60% 40% / 30% 70% 40% 60%; }
  100% { border-radius: 60% 40% 70% 30% / 50% 60% 40% 50%; }
}
@keyframes ga-13-morph-a2 {
  0%   { border-radius: 40% 60% 50% 50% / 60% 40% 60% 40%; }
  33%  { border-radius: 60% 40% 70% 30% / 40% 60% 30% 70%; }
  66%  { border-radius: 30% 70% 40% 60% / 70% 30% 60% 40%; }
  100% { border-radius: 40% 60% 50% 50% / 60% 40% 60% 40%; }
}
@keyframes ga-13-morph-b1 {
  0%   { border-radius: 55% 45% 65% 35% / 45% 55% 45% 55%; }
  30%  { border-radius: 35% 65% 45% 55% / 65% 35% 65% 35%; }
  60%  { border-radius: 65% 35% 55% 45% / 35% 65% 35% 65%; }
  100% { border-radius: 55% 45% 65% 35% / 45% 55% 45% 55%; }
}
@keyframes ga-13-morph-b2 {
  0%   { border-radius: 45% 55% 35% 65% / 55% 45% 65% 35%; }
  40%  { border-radius: 65% 35% 55% 45% / 35% 65% 45% 55%; }
  80%  { border-radius: 35% 65% 45% 55% / 65% 35% 55% 45%; }
  100% { border-radius: 45% 55% 35% 65% / 55% 45% 65% 35%; }
}
@keyframes ga-13-morph-c1 {
  0%   { border-radius: 50% 50% 60% 40% / 40% 60% 50% 50%; }
  35%  { border-radius: 70% 30% 40% 60% / 60% 40% 70% 30%; }
  70%  { border-radius: 40% 60% 70% 30% / 30% 70% 40% 60%; }
  100% { border-radius: 50% 50% 60% 40% / 40% 60% 50% 50%; }
}
@keyframes ga-13-morph-c2 {
  0%   { border-radius: 60% 40% 50% 50% / 50% 50% 40% 60%; }
  45%  { border-radius: 40% 60% 60% 40% / 60% 40% 50% 50%; }
  90%  { border-radius: 50% 50% 40% 60% / 40% 60% 60% 40%; }
  100% { border-radius: 60% 40% 50% 50% / 50% 50% 40% 60%; }
}

/* Float keyframes (position) */
@keyframes ga-13-float-a1 {
  0%, 100% { top: 60%; }
  30%  { top: 10%; }
  60%  { top: 40%; }
}
@keyframes ga-13-float-a2 {
  0%, 100% { top: 18%; }
  40%  { top: 65%; }
  70%  { top: 25%; }
}
@keyframes ga-13-float-a3 {
  0%, 100% { top: 42%; }
  25%  { top: 5%; }
  55%  { top: 70%; }
  80%  { top: 30%; }
}
@keyframes ga-13-float-b1 {
  0%, 100% { top: 55%; left: 35px; }
  35%  { top: 8%; left: 45px; }
  65%  { top: 35%; left: 25px; }
}
@keyframes ga-13-float-b2 {
  0%, 100% { top: 15%; left: 50px; }
  45%  { top: 62%; left: 40px; }
  75%  { top: 22%; left: 55px; }
}
@keyframes ga-13-float-c1 {
  0%, 100% { top: 58%; }
  28%  { top: 6%; }
  58%  { top: 38%; }
}
@keyframes ga-13-float-c2 {
  0%, 100% { top: 18%; }
  38%  { top: 62%; }
  72%  { top: 28%; }
}

/* Speed toggle */
.ga-13__ctrl {
  position: absolute;
  bottom: 14px;
  right: 14px;
  display: flex;
  gap: 5px;
}
.ga-13__ctrl-btn {
  padding: 4px 10px;
  font-size: .7rem;
  font-weight: 700;
  border-radius: 6px;
  border: 1px solid rgba(255,255,255,.1);
  background: rgba(255,255,255,.05);
  color: rgba(255,255,255,.35);
  cursor: pointer;
  transition: all .2s;
}
.ga-13__ctrl-btn.active,
.ga-13__ctrl-btn:hover {
  background: rgba(249,115,22,.15);
  border-color: rgba(249,115,22,.35);
  color: #fb923c;
}

@media (prefers-reduced-motion: reduce) {
  .ga-13__blob { animation: none !important; border-radius: 50%; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%); }
}
(function() {
  const w = document.querySelector('.ga-13');
  w.querySelectorAll('.ga-13__ctrl-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      w.querySelectorAll('.ga-13__ctrl-btn').forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
      w.style.setProperty('--dur', btn.dataset.dur);
    });
  });
})();

How this works

Each blob is a div.ga-13__blob with two independent animation directives running in parallel via comma-separated values on the animation property: a morph animation changes border-radius through four asymmetric corner values (e.g. 60% 40% 70% 30% / 50% 60% 40% 50%) making the blob squish and stretch organically, while a float animation changes top percentage from near the bottom of the lamp to near the top and back, simulating the buoyancy cycle of a heated wax blob. Having both animations run at different durations and with separate delay offsets on each blob ensures the three blobs in each lamp are never synchronised.

The radial-gradient on each blob uses a slightly off-centre source position (at 35% 40%) to create an asymmetric highlight, making the blobs look like they have a bright face and a shadowed back — mimicking the internal light source of a real lava lamp. A small filter: blur(2px) on every blob softens the morphing edges so rapid border-radius changes read as fluid rather than mechanical.

Customize

  • Add a fourth blob to any lamp by duplicating a .ga-13__blob div and giving it a new size, starting position, and animation-delay — vary the float keyframe target by adjusting the top percentage in the matching @keyframes ga-13-float-*.
  • Change each lamp colour by editing the radial-gradient stops on the blob children — combine a bright inner highlight colour with a darker outer stop to maintain the three-dimensional feel.
  • Speed up the entire scene by reducing --dur on .ga-13 from 8s to 4s — all blob durations are multiples of this base value via CSS calc(), so the relative timing relationships are preserved.
  • Increase the morph expressiveness by adding a fifth keyframe step in the border-radius animations — use even more extreme corner ratios like 80% 20% 90% 10% / 20% 80% 15% 85% for blobs that nearly fold in on themselves.
  • Add a base heat-glow to each lamp by placing a small absolutely-positioned div at the bottom with a radial gradient matching the blob colour — this simulates the heat source that drives real lava lamps.

Watch out for

  • Animating top (a layout property) on the blobs triggers layout recalculation on every frame in older browsers — for maximum performance, switch to transform: translateY() by converting the float keyframes to percentage offsets and applying an initial top: 50% reference point.
  • The eight-value border-radius syntax (60% 40% 70% 30% / 50% 60% 40% 50%) specifies horizontal and vertical radii independently — some CSS linters incorrectly flag this as invalid; it is the correct CSS specification syntax and works in all modern browsers.
  • filter: blur(2px) on each blob creates a new stacking context and can cause blobs to render above absolutely-positioned siblings even without explicit z-index — if blob labels disappear behind blobs, add z-index: 3 to the label elements.

Browser support

ChromeSafariFirefoxEdge
49+ 9.1+ 36+ 49+

The eight-value border-radius syntax is supported in all modern browsers (Chrome 4+, Safari 5+, Firefox 3.5+). Radial gradients and CSS animations are universally supported.

Search CodeFronts

Loading…