<script type="houdini/paint-worklet">
registerPaint('btn-pulse', class {
  static get inputProperties() {
    return ['--x', '--y', '--rad', '--pulse-rad', '--fill', '--pulse-stroke-color']
  }

  _paintCircles(ctx, fillStyle, strokeStyle, x, y, rad) {
    ctx.beginPath()
    ctx.fillStyle = fillStyle
    ctx.strokeStyle = strokeStyle
    ctx.arc(x, y, rad, 0, 2 * Math.PI)
    ctx.fill()
    ctx.stroke()
  }

  paint(ctx, size, props) {
    const x = props.get('--x')
    const y = props.get('--y')
    const rad = props.get('--rad')
    const fill = (props.get('--fill')).toString()
    const pulseRad = props.get('--pulse-rad')
    const pulseStrokeColor = (props.get('--pulse-stroke-color')).toString()

    this._paintCircles(ctx, fill, '#fff', x.value, y.value, rad.value)
    this._paintCircles(ctx, 'transparent', pulseStrokeColor, x.value, y.value, pulseRad.value + rad.value)
  }
})
</script>
<button class="button" type="button">hover/click me</button>
<button class="button button--dark" type="button">hover/click me</button>
<button class="button button--fill" type="button">hover/click me</button>
body {
  margin: 0;
  height: 100%;
  font-family: 'Nunito', sans-serif;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
}

.button {
  appearance: none;
  cursor: none;
  border: 0;
  padding: .3em;
  margin: 1em;
  font-family: 'Nunito', sans-serif;
  font-size: clamp(1.2em, 3vw, 2em);
  display: flex;
  justify-content: center;
  align-items: center;
  width: clamp(320px, 400px, 100%);
  height: 4em;
  color: #fff;
  --x: -20;
  --y: -20;
  --rad: 0;
  --pulse-rad: 0;
  --pulse-stroke-color: rgba(255, 255, 255, 1);
  background: tomato paint(btn-pulse);
  transition: --rad linear .2s;
  &:focus {
    outline: none;
  }
  &--dark {
    background: #260d00 paint(btn-pulse);
  }
  &--fill {
    --fill: #4b4ea5;
  }
  &.hover {
    animation: pulse 1s linear infinite;
  }
}

@keyframes pulse {
  0% {
    --pulse-stroke-color: rgba(255, 255, 255, 1);
  }
  60% {
    --pulse-rad: calc(var(--rad) + 5);
    --pulse-stroke-color: rgba(255, 255, 255, 0);
  }
  100% {
    --pulse-rad: calc(var(--rad) + 5);
    --pulse-stroke-color: rgba(255, 255, 255, 0);
  }
}
View Compiled
// PaintWorklet is in Codepen HTML block
// Code on GitHub: https://github.com/ivanalbizu/css-houdini-button-pulse-animation

(async () => {
  if (typeof CSS === 'undefined' || !('paintWorklet' in CSS)) {
    await import("https://unpkg.com/css-paint-polyfill");
  }

  const paintModule = URL.createObjectURL(new Blob(
    [ document.querySelector('[type="houdini/paint-worklet"]').textContent ],
    { type: "text/javascript" }
  ));
  await CSS.paintWorklet.addModule(paintModule);
  
  window.CSS.registerProperty({
    name: '--x',
    syntax: '<integer>',
    inherits: false,
    initialValue: '-20'
  })
  window.CSS.registerProperty({
    name: '--y',
    syntax: '<integer>',
    inherits: false,
    initialValue: '-20'
  })
  window.CSS.registerProperty({
    name: '--rad',
    syntax: '<integer>',
    inherits: false,
    initialValue: '0'
  })
  window.CSS.registerProperty({
    name: '--pulse-rad',
    syntax: '<integer>',
    inherits: false,
    initialValue: '0'
  })
  window.CSS.registerProperty({
    name: '--fill',
    syntax: '<color>',
    inherits: false,
    initialValue: 'rgba(0,0,0,.4)'
  })
  window.CSS.registerProperty({
    name: '--pulse-stroke-color',
    syntax: '<color>',
    inherits: false,
    initialValue: 'rgba(255, 255, 255, 1)'
  })

  const buttons = document.querySelectorAll('.button')
  buttons.forEach(button => {
    button.addEventListener('click', event => {
      const rad = parseInt(getComputedStyle(event.target).getPropertyValue('--rad'), 10)
      const pulseRad = parseInt(getComputedStyle(event.target).getPropertyValue('--pulse-rad'), 10)
      event.target.style.setProperty('--rad', `${rad + 5}`)
      event.target.style.setProperty('--pulse-rad', `${pulseRad + 5}`)
    })
    button.addEventListener('mousemove', event => {
      event.target.style.setProperty('--x', event.offsetX)
      event.target.style.setProperty('--y', event.offsetY)
    })
    button.addEventListener('mouseenter', event => {
      event.target.classList.add('hover')
      event.target.style.setProperty('--rad', 10)
    })
    button.addEventListener('mouseleave', event => {
      event.target.classList.remove('hover')
      event.target.style.setProperty('--rad', 0)
      event.target.style.setProperty('--pulse-rad', 0)
    })
  })

})()
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.