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.
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.
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>
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); }
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 }); }); })();
| Where | Default | Effect |
|---|---|---|
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. |
Same physics, different palette. Click any to try.