SWAMP UI / COMPONENTS / POND RIPPLE

Pond Ripple

every click is a stone in still water

Click anywhere and water ripples expand outward, with a soft plop sound. It's the poetic cousin of Material Design's ripple — same idea, more patience, with a whisper of audio.

No dependencies
~1 KB CSS + 1 KB JS
Sound via Web Audio
Works on any element
LIVE PREVIEW · CLICK THE WATER
click anywhere on the water ↑
works on any element ↓
A card
A link

When to use

Reach for it on any click target where you want delight to confirm the action — buttons, cards, links, image tiles. The plop sound makes it feel like you actually touched something. Especially great for hero CTAs, "favorite" buttons, or anywhere a Material ripple would feel too corporate.

Skip it when the page is high-frequency (tables, lists with hundreds of rows) or when sound would be intrusive (forms inside an admin tool). You can also disable sound globally with one parameter.

Skip to

1. HTML

Add the class pond-click to any element you want to ripple on click. That's it.

<!-- works on anything -->
<button class="pond-click">Submit</button>

<div class="pond-click my-card">A card</div>

<!-- big "pond" area -->
<div class="pond-click my-pond"></div>

<!-- silent variant (no plop) -->
<button class="pond-click" data-silent>Quiet</button>

<!-- color variants -->
<button class="pond-click" data-color="lime">Lime</button>

2. CSS

Element needs position:relative + overflow:hidden so the ripple stays inside. The ripple itself is one keyframe.

.pond-click {
  position: relative;
  overflow: hidden;
  cursor: pointer;
}

.pond-ripple {
  position: absolute;
  border-radius: 50%;
  border: 1.5px solid #52ffe0;     /* cyan default */
  background: rgba(82, 255, 224, .06);
  pointer-events: none;
  transform: translate(-50%, -50%);
  width: 6px; height: 6px;
  animation: pond-out 1.5s ease-out forwards;
}

@keyframes pond-out {
  0%   { width: 6px;   height: 6px;   opacity: .95; border-width: 2px;  }
  100% { width: 280px; height: 280px; opacity: 0;   border-width: .5px; }
}

/* optional color variants */
.pond-click[data-color="lime"] .pond-ripple {
  border-color: #b6ff5b;
  background: rgba(182, 255, 91, .06);
}
.pond-click[data-color="pink"] .pond-ripple {
  border-color: #ff4d8d;
  background: rgba(255, 77, 141, .06);
}

3. JavaScript

Listens for clicks on anything with .pond-click. Creates a ripple at the click point, plays a soft plop via Web Audio. No audio files needed.

// Swamp UI — Pond Ripple
(function () {
  let audioCtx = null;
  function plop(volume) {
    try {
      if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      const osc  = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.connect(gain).connect(audioCtx.destination);
      osc.type = 'sine';
      osc.frequency.setValueAtTime(180, audioCtx.currentTime);
      osc.frequency.exponentialRampToValueAtTime(55, audioCtx.currentTime + 0.18);
      gain.gain.setValueAtTime(volume || 0.18, audioCtx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.28);
      osc.start();
      osc.stop(audioCtx.currentTime + 0.3);
    } catch (e) { /* audio not available — silent */ }
  }

  function ripple(e) {
    const el   = e.currentTarget;
    const rect = el.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    const r = document.createElement('span');
    r.className = 'pond-ripple';
    r.style.left = x + 'px';
    r.style.top  = y + 'px';
    el.appendChild(r);
    setTimeout(() => r.remove(), 1500);

    if (!el.hasAttribute('data-silent')) plop();
  }

  // Wire up all .pond-click elements (now & future)
  document.addEventListener('click', e => {
    const el = e.target.closest('.pond-click');
    if (!el) return;
    ripple({ currentTarget: el, clientX: e.clientX, clientY: e.clientY });
  });
})();

4. Parameters you can tweak

WhereDefaultEffect
CSS @keyframes pond-out width/height 280px How big the ripple grows. Smaller = more local; bigger = pond-wide reach.
CSS animation duration 1.5s How long each ripple lives. Shorter = snappier feedback.
JS plop() frequency 180 → 55 Hz Pitch of the plop. Higher = bright drop, lower = bass thunk.
JS volume default 0.18 How loud the plop is (0–1). Anything above .25 starts feeling rude.
HTML data-silent Add to any element to disable its plop. Ripples still play visually.
HTML data-color="lime|pink|cream" cyan Change ripple color per element without rewriting CSS.

5. Variants

Same physics, different palette. Click any to try.

click ↑
CYAN (default)
click ↑
LIME
click ↑
PINK
click ↑ (silent)
CREAM · SILENT
Liked this? Feed Mr. Knife & Ms. Ling.
And buy their owner a milk tea.
🧋 TREAT THE SWAMP
More in the garden → Firefly Cursor, Firefly Tour, Xylophone Lights, Cat Wanderer, Swamp Name Generator.
Browse all →