<div class="no-dice" hidden>
  You need <a href="https://drafts.css-houdini.org/css-paint-api/"><code>PaintWorklet</code></a> to see this demo :(
</div>
<button type="button">Hello</button>

<script type="houdini/worklet">
  registerPaint('ripple', class {
    static get inputProperties() {
      return [
        '--ripple-color',
        '--ripple-radius',
        '--ripple-center-x',
        '--ripple-center-y'
      ];
    }
    paint(ctx, { width, height }, props) {
      const color = props.get('--ripple-color');
      const radius = props.get('--ripple-radius');
      const centerX = props.get('--ripple-center-x');
      const centerY = props.get('--ripple-center-y');
      const farthest = (Math.max(centerX, width - centerX) ** 2
        + Math.max(centerY, height - centerY) ** 2) ** .5;
      ctx.fillStyle = color;
      ctx.arc(centerX.value, centerY.value, radius.value * farthest / 100, 0, Math.PI * 2);
      ctx.fill();
    }
  });
</script>
html, body {
  height: 100%;
}
body {
  margin: 0;
  font-family: system-ui, sans-serif;
  display: flex;
  justify-content: center;
  align-items: center;
}
button {
  border: none;
  border-radius: .25em;
  font-size: 4em;
  width: 10em;
  padding: .25em 0;
  background-image: paint(ripple);
  background-color: red;
  color: white;
}
View Compiled
(async () => {
  if (typeof CSS === 'undefined' || !('paintWorklet' in CSS)) {
    document.querySelector('.no-dice').hidden = false;
    return;
  }
  
  CSS.registerProperty({
    name: '--ripple-color',
    syntax: '<color>',
    initialValue: 'rgba(255, 255, 255, 0.5)',
    inherits: false
  });
  CSS.registerProperty({
    name: '--ripple-center-x',
    syntax: '<number>',
    initialValue: 0,
    inherits: false
  });
  CSS.registerProperty({
    name: '--ripple-center-y',
    syntax: '<number>',
    initialValue: 0,
    inherits: false
  });
  CSS.registerProperty({
    name: '--ripple-radius',
    syntax: '<percentage>',
    initialValue: '0%',
    inherits: false
  });

  const houdiniModule = URL.createObjectURL(new Blob(
    [ document.querySelector('[type="houdini/worklet"]').textContent ],
    { type: "text/javascript" }
  ));
  await CSS.paintWorklet.addModule(houdiniModule);

  const button = document.querySelector('button');
  button.addEventListener('click', ({ clientX, clientY }) => {
    const { top, left } = button.getBoundingClientRect();
    const offsetX = clientX - left;
    const offsetY = clientY - top;
    // It used to accept `CSS.number(offsetX)` rather than just `offsetX`, but
    // now... it doesn't.
    button.attributeStyleMap.set('--ripple-center-x', offsetX);
    button.attributeStyleMap.set('--ripple-center-y', offsetY);
    button.animate([
      {
        '--ripple-color': 'rgba(255, 255, 255, 0.5)',
        '--ripple-radius': CSS.percent(0)
      },
      {
        '--ripple-color': 'rgba(255, 255, 255, 0)',
        '--ripple-radius': CSS.percent(100)
      }
    ], {
      duration: 1000
    });
  });
})();

External CSS

  1. https://fonts.googleapis.com/css?family=Inter:400,500,600,700&amp;display=swap

External JavaScript

This Pen doesn't use any external JavaScript resources.