← Buttons

Click Ripple

A circle of light spreads out from exactly where a button was pressed, then fades, so every tap gets clear, physical feedback.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-click-ripple to buttons or any clickable element.

Options

Add any of these attributes to the same element.

data-ripple-colorRipple colour. Default white at 35%. Use currentColor on light buttons.
data-durationHow long a ripple lasts. Default 700 ms.

From code, animacious.ripple(element) fires one from the centre.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Click Ripple: a circle of light spreads out from exactly where a button was
    // pressed, then fades, so every tap gets visible feedback.
    // Usage: <button data-click-ripple>Save</button>
    // Optional: data-ripple-color (default: white at 35%, or the text colour on
    // light buttons via data-ripple-color="currentColor"), data-duration (ms).
    // Script use: animacious.ripple(element, x, y) fires one from page coordinates.
    var DEFAULTS = { duration: 700, color: 'rgba(255,255,255,.35)' };

    var css =
      '[data-click-ripple]{position:relative;overflow:hidden;isolation:isolate}' +
      '.rp-dot{position:absolute;border-radius:50%;pointer-events:none;z-index:-1;transform:scale(0)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function ripple(el, clientX, clientY) {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      var r = el.getBoundingClientRect();
      var x = clientX === undefined ? r.width / 2 : clientX - r.left;
      var y = clientY === undefined ? r.height / 2 : clientY - r.top;
      // Big enough to reach the farthest corner from the press point.
      var size = 2 * Math.hypot(Math.max(x, r.width - x), Math.max(y, r.height - y));
      var dot = document.createElement('span');
      dot.className = 'rp-dot';
      var color = el.dataset.rippleColor || DEFAULTS.color;
      dot.style.cssText += 'left:' + (x - size / 2) + 'px;top:' + (y - size / 2) + 'px;width:' + size + 'px;height:' + size + 'px;' +
        'background:' + color + (color === 'currentColor' ? ';opacity:.25' : '');
      el.appendChild(dot);
      var duration = +el.dataset.duration || DEFAULTS.duration;
      anime.animate(dot, { scale: [0, 1], duration: duration, ease: 'out(3)' });
      anime.animate(dot, {
        opacity: 0,
        duration: duration * 0.6,
        delay: duration * 0.4,
        ease: 'linear',
        onComplete: function () { dot.remove(); }
      });
    }

    document.addEventListener('pointerdown', function (e) {
      var el = e.target.closest && e.target.closest('[data-click-ripple]');
      if (el) ripple(el, e.clientX, e.clientY);
    });
    // Keyboard presses ripple from the centre.
    document.addEventListener('keydown', function (e) {
      if (e.key !== 'Enter' && e.key !== ' ') return;
      var el = e.target.closest && e.target.closest('[data-click-ripple]');
      if (el) ripple(el);
    });

    window.animacious = window.animacious || {};
    window.animacious.ripple = ripple;
  })();
</script>
2 · Your HTML
<button data-click-ripple>Save changes</button>