3 Pure CSS4 with JSPublished 2026-10-05Free · no sign-up
Seven ripples: the touch ripple that starts where you click, a script-free press ripple, pulse rings around a map pin, a ripple across a settings row, a theme switch that spreads in a circle, a sonar ping on a status dot and bounded ripples on round icon buttons.
The classic touch ripple: a circle grows from exactly where the button was pressed and fades out. Script reads the pointer position, places a span there sized to cover the button and removes it when its animation ends. Keyboard presses ripple from the centre.
document.querySelector('.rp1').addEventListener('click', function (e) {
var b = this, r = b.getBoundingClientRect(), s = Math.max(r.width, r.height) * 2, d = document.createElement('span');
var x = e.clientX ? e.clientX - r.left : r.width / 2, y = e.clientY ? e.clientY - r.top : r.height / 2;
d.className = 'rp'; d.style.cssText = 'width:' + s + 'px;height:' + s + 'px;left:' + (x - s / 2) + 'px;top:' + (y - s / 2) + 'px';
b.appendChild(d); d.addEventListener('animationend', function () { d.remove(); });
});
02. CSS-Only Press Ripple
RippleNo JS
A ripple with no script: a radial gradient background is huge and transparent at rest, and on :active it snaps small with the transition removed, then grows again when the press ends. It always starts from the centre, which is fine for small buttons.
Two rings spread out from a map pin and fade, a continuous ripple that says this is where you are. The rings are pseudo-elements with the same animation offset by half its duration, so one is always growing while the other fades.
The same click ripple on a larger surface: a settings row that is a single button. The ripple colour is a translucent tint of the accent so it shows on the dark card, and the row's label and value stay readable through it.
document.querySelector('.rp4').addEventListener('click', function (e) {
var r = this.getBoundingClientRect(), s = Math.max(r.width, r.height) * 2.2, d = document.createElement('span');
var x = e.clientX ? e.clientX - r.left : r.width / 2, y = e.clientY ? e.clientY - r.top : r.height / 2;
d.className = 'rp'; d.style.cssText = 'width:' + s + 'px;height:' + s + 'px;left:' + (x - s / 2) + 'px;top:' + (y - s / 2) + 'px';
this.appendChild(d); d.addEventListener('animationend', function () { d.remove(); });
});
05. Theme Switch with Circular Reveal
RippleTheme
Switching the panel to dark mode spreads the new theme out from the toggle in a growing circle, made with clip-path: circle() on an overlay. The toggle is a real switch with role="switch" and aria-checked, and the colours change instantly for reduced motion.
var w = document.querySelector('.rp5'), b = w.querySelector('button');
b.onclick = function () { var on = b.getAttribute('aria-checked') !== 'true'; b.setAttribute('aria-checked', on); w.classList.toggle('dark', on); };
06. Sonar Ping on a Status Dot
RippleStatus
A small green dot sends out a sonar ping to show something is live, such as a deploy in progress or an agent online. The label next to it says the status in words, and the ping is a single scaled box-shadow ring on a pseudo-element.
View code
<p class="rp6"><span class="d" aria-hidden="true"></span>Deploying to production</p>
A toolbar of round icon buttons where the ripple is clipped to each circle. One script handles all of them through event delegation on the toolbar, and each button has an aria-label naming its action.
document.querySelector('.rp7').addEventListener('click', function (e) {
var b = e.target.closest('button'); if (!b) return;
var d = document.createElement('span'); d.className = 'rp'; b.appendChild(d);
d.addEventListener('animationend', function () { d.remove(); });
});
About these CSS Ripple Effects
Ripples are feedback
A ripple confirms that a press registered, which matters most on touch screens where there is no hover. It should start immediately and finish quickly, under about 600ms, so it never delays the action itself.
Keep it inside the shape
Give the button position: relative and overflow: hidden, then place the ripple span absolutely. The rounded corners clip it, and the span is removed when its animationend event fires.
Keyboard presses have no coordinates
A click triggered by Enter or Space reports a pointer position of zero. Detect that and start the ripple from the centre instead of the top-left corner.
Continuous ripples need restraint
Pulsing rings on a map pin or status dot loop forever, so keep them subtle and stop them under prefers-reduced-motion. The meaning (you are here, live) must also be in text.
Frequently asked questions
Can I make a ripple without JavaScript?
Yes, from the centre: the CSS-only demo animates a radial-gradient background on :active.
Does the ripple slow the button down?
No. It runs on transform and opacity, and the click handler runs before the animation starts.
Why remove the ripple span?
Each click adds one; removing it on animationend stops hundreds piling up in the DOM.