12 CSS Ripple Effects 01 / 12

CSS Water Ripple Click Effect

An interactive water-surface pond you click to create realistic multi-ring ripples — four concentric CSS rings (box-shadow glow, fade-out scale) spawn at the exact cursor coordinate via JS, plus a radial-gradient caustic shimmer overlay and animated floating bubble particles.

CSS + JS MIT licensed
Live Demo Open in tab

This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.

Open in playground

The code

<div class="rpl-01">
  <div class="rpl-01__header">
    <h1>Water Ripple Click Effect</h1>
    <p>Click anywhere on the surface to create realistic water ripples</p>
  </div>

  <div class="rpl-01__pond" id="rpl-01-pond">
    <div class="rpl-01__label">click the surface</div>
  </div>

  <div class="rpl-01__info">
    <div class="rpl-01__stat">
      <b id="rpl-01-count">0</b>
      <span>Drops</span>
    </div>
    <div class="rpl-01__stat">
      <b id="rpl-01-rings">0</b>
      <span>Active Rings</span>
    </div>
  </div>
</div>
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap');

.rpl-01, .rpl-01 *, .rpl-01 *::before, .rpl-01 *::after { box-sizing: border-box; margin: 0; padding: 0; }
.rpl-01 ::selection { background: #00d4ff; color: #001a2e; }

.rpl-01 {
  --deep: #001a2e;
  --mid: #003a5c;
  --water: #00a8cc;
  --foam: #00d4ff;
  --glint: #7ef7ff;
  --text: #e0f7ff;
  font-family: 'Outfit', sans-serif;
  background: radial-gradient(ellipse at 50% 0%, #003a5c, #001015 70%);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 40px;
  padding: 40px 20px;
  color: var(--text);
  overflow: hidden;
  position: relative;
}

/* Animated depth lines */
.rpl-01::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(0deg, transparent 0px, transparent 48px, rgba(0,168,204,0.06) 48px, rgba(0,168,204,0.06) 50px);
  pointer-events: none;
}

.rpl-01__header {
  text-align: center;
  z-index: 2;
}
.rpl-01__header h1 {
  font-size: clamp(1.8rem, 5vw, 3rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  background: linear-gradient(135deg, var(--glint), var(--foam), var(--water));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.rpl-01__header p {
  margin-top: 8px;
  font-size: 1rem;
  color: rgba(224,247,255,0.55);
  font-weight: 300;
}

/* The water surface */
.rpl-01__pond {
  width: min(700px, 100%);
  height: 400px;
  position: relative;
  border-radius: 20px;
  overflow: hidden;
  cursor: crosshair;
  border: 1px solid rgba(0,212,255,0.2);
  box-shadow: 0 0 60px rgba(0,168,204,0.25), inset 0 0 80px rgba(0,20,40,0.6);
  background:
    radial-gradient(ellipse at 50% 0%, rgba(0,168,204,0.18), transparent 60%),
    linear-gradient(180deg, #001f38 0%, #000e1a 100%);
  z-index: 2;
}

/* Caustic shimmer overlay */
.rpl-01__pond::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 200% 80% at 50% 100%, rgba(0,168,204,0.12), transparent),
    repeating-linear-gradient(60deg, transparent 0px, transparent 20px, rgba(0,212,255,0.03) 20px, rgba(0,212,255,0.03) 21px),
    repeating-linear-gradient(-60deg, transparent 0px, transparent 20px, rgba(0,212,255,0.03) 20px, rgba(0,212,255,0.03) 21px);
  animation: rpl-01-caustic 8s linear infinite;
  pointer-events: none;
  z-index: 1;
}
@keyframes rpl-01-caustic { to { background-position: 0 40px, 30px 0, -30px 0; } }

.rpl-01__pond::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, transparent 60%, rgba(0,10,20,0.5) 100%);
  pointer-events: none;
  z-index: 3;
}

/* A ripple ring spawned by JS */
.rpl-01__ripple {
  position: absolute;
  border-radius: 50%;
  transform: translate(-50%, -50%) scale(0);
  pointer-events: none;
  z-index: 2;
  animation: rpl-01-wave 2.4s cubic-bezier(0.2, 0.6, 0.4, 1) forwards;
}
@keyframes rpl-01-wave {
  0%   { transform: translate(-50%,-50%) scale(0); opacity: 1; }
  100% { transform: translate(-50%,-50%) scale(1); opacity: 0; }
}

/* Three concentric ring sizes */
.rpl-01__ripple--a {
  width: 60px; height: 60px;
  border: 2px solid rgba(0,212,255,0.9);
  box-shadow: 0 0 12px rgba(0,212,255,0.5), inset 0 0 12px rgba(0,212,255,0.1);
}
.rpl-01__ripple--b {
  width: 130px; height: 130px;
  border: 1.5px solid rgba(0,168,204,0.65);
  box-shadow: 0 0 20px rgba(0,168,204,0.3);
  animation-delay: 0.12s;
}
.rpl-01__ripple--c {
  width: 220px; height: 220px;
  border: 1px solid rgba(0,120,160,0.4);
  animation-delay: 0.26s;
}
.rpl-01__ripple--d {
  width: 340px; height: 340px;
  border: 0.5px solid rgba(0,90,120,0.25);
  animation-delay: 0.44s;
}

/* Center drop dot */
.rpl-01__drop {
  position: absolute;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: radial-gradient(circle at 35% 30%, #7ef7ff, #00d4ff);
  transform: translate(-50%, -50%) scale(0);
  pointer-events: none;
  z-index: 4;
  animation: rpl-01-drop 0.4s cubic-bezier(0.2, 0.9, 0.4, 1) forwards;
  box-shadow: 0 0 14px #00d4ff;
}
@keyframes rpl-01-drop {
  0%   { transform: translate(-50%,-50%) scale(0); opacity: 1; }
  60%  { transform: translate(-50%,-50%) scale(1.4); opacity: 1; }
  100% { transform: translate(-50%,-50%) scale(0.6); opacity: 0; }
}

.rpl-01__label {
  position: absolute;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.75rem;
  color: rgba(0,212,255,0.4);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  z-index: 4;
  pointer-events: none;
  animation: rpl-01-pulse 2s ease-in-out infinite;
}
@keyframes rpl-01-pulse { 50% { opacity: 0.2; } }

/* Ambient floating particles */
.rpl-01__particle {
  position: absolute;
  border-radius: 50%;
  background: rgba(0,212,255,0.6);
  pointer-events: none;
  z-index: 1;
  animation: rpl-01-float linear infinite;
}
@keyframes rpl-01-float {
  0% { transform: translateY(0) scale(1); opacity: 0.6; }
  50% { opacity: 0.2; }
  100% { transform: translateY(-380px) scale(0.3); opacity: 0; }
}

.rpl-01__info {
  display: flex;
  gap: 32px;
  z-index: 2;
  flex-wrap: wrap;
  justify-content: center;
}
.rpl-01__stat {
  text-align: center;
}
.rpl-01__stat b {
  display: block;
  font-size: 1.8rem;
  font-weight: 700;
  color: var(--foam);
}
.rpl-01__stat span {
  font-size: 0.75rem;
  color: rgba(224,247,255,0.4);
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

@media (prefers-reduced-motion: reduce) {
  .rpl-01__pond::before, .rpl-01__label, .rpl-01__particle { animation: none !important; }
  .rpl-01__ripple, .rpl-01__drop { animation-duration: 0.01ms !important; }
}
(() => {
  const pond = document.getElementById('rpl-01-pond');
  const countEl = document.getElementById('rpl-01-count');
  const ringsEl = document.getElementById('rpl-01-rings');
  let dropCount = 0;
  let activeRings = 0;

  // Spawn ambient particles
  for (let i = 0; i < 12; i++) {
    const p = document.createElement('div');
    p.className = 'rpl-01__particle';
    const size = Math.random() * 4 + 1;
    p.style.cssText = `
      width:${size}px; height:${size}px;
      left:${Math.random()*100}%;
      bottom:${Math.random()*100}%;
      opacity:${Math.random()*0.6+0.1};
      animation-duration:${Math.random()*6+4}s;
      animation-delay:${Math.random()*-6}s;
    `;
    pond.appendChild(p);
  }

  pond.addEventListener('click', (e) => {
    const rect = pond.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    dropCount++;
    countEl.textContent = dropCount;

    // spawn drop dot
    const drop = document.createElement('div');
    drop.className = 'rpl-01__drop';
    drop.style.left = x + 'px';
    drop.style.top = y + 'px';
    pond.appendChild(drop);

    // spawn 4 concentric rings
    ['a','b','c','d'].forEach((t, i) => {
      const r = document.createElement('div');
      r.className = `rpl-01__ripple rpl-01__ripple--${t}`;
      r.style.left = x + 'px';
      r.style.top = y + 'px';
      pond.appendChild(r);
      activeRings++;
      ringsEl.textContent = activeRings;
      r.addEventListener('animationend', () => {
        r.remove();
        activeRings = Math.max(0, activeRings - 1);
        ringsEl.textContent = activeRings;
      }, { once: true });
    });

    drop.addEventListener('animationend', () => drop.remove(), { once: true });
  });
})();

Search CodeFronts

Loading…