<a href="#" class="button">
  <canvas class="button__canvas"></canvas>
  <span class="button__text">Hover me I am liquid</span>
</a>
body {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: #eef;
}

.button {
  
  position: relative;
  padding: 1.6em 2.8em;
  text-decoration: none;

  &__canvas {
    --offset: 32px;
    
    position: absolute;
    top: calc(var(--offset) * -1);
    left: calc(var(--offset) * -1);
    width: calc(100% + var(--offset) * 2);
    height: calc(100% + var(--offset) * 2);
    transition: opacity 2s ease;
  }
  
  &__text {
    position: relative;
    color: white;
    font-weight: bold;
    letter-spacing: .02em;
    pointer-events: none;
  }
  
  &:hover &__canvas {
    opacity: .85;
    transition: opacity .2s ease;
  }
  
  &:active &__canvas {
    opacity: 1;
    transition: none;
  }
  
}
View Compiled
document.querySelectorAll('.button').forEach((elem) => {

  const canvas = elem.querySelector('.button__canvas')
  const ctx = canvas.getContext('2d')
  
  const offset = 32
  const background = '#eef'
  const foreground = '#7551e9'

  let points = []
  let position
  
  const distance = new Ola({
    value: offset
  })

  const size = () => {

    canvas.width = canvas.getBoundingClientRect().width
    canvas.height = canvas.getBoundingClientRect().height
    
    position = new Ola({
      x: canvas.width / 2,
      y: canvas.height / 2
    })
    
    const pixelsWidth = canvas.width - offset * 2
    const pixelsHeight = canvas.height - offset * 2
    
    const leftTop = [ offset, offset ]
    const rightTop = [ canvas.width - offset, offset ]
    const rightBottom = [ canvas.width - offset, canvas.height - offset ]
    const leftBottom = [ offset, canvas.height - offset ]
    
    points = []

    Array(pixelsWidth).fill().forEach((_, index) => {
      points.push([
        leftTop[0] + index,
        leftTop[1]
      ])
    })
    
    Array(pixelsHeight).fill().forEach((_, index) => {
      points.push([
        rightTop[0],
        rightTop[1] + index
      ])
    })
    
    Array(pixelsWidth).fill().forEach((_, index) => {
      points.push([
        rightBottom[0] - index,
        rightBottom[1]
      ])
    })
    
    Array(pixelsHeight).fill().forEach((_, index) => {
      points.push([
        leftBottom[0],
        leftBottom[1] - index
      ])
    })
    
  }

  size()
  
  const reset = () => {

    ctx.fillStyle = background
    ctx.fillRect(0, 0, canvas.width, canvas.height)

  }

  const draw = () => {

    ctx.fillStyle = foreground
    ctx.beginPath()
    
    points.forEach((point, index) => {
      
      const [ vx, vy ] = attract(point)
            
      if (index === 0) ctx.moveTo(vx, vy)
      else ctx.lineTo(vx, vy)
            
    })
    
    ctx.closePath()
    ctx.fill()

  }
  
  const attract = (point) => {
        
    const [ x, y ] = point

    const dx = x - position.x
    const dy = y - position.y
    
    const dist = Math.sqrt(dx * dx + dy * dy)
    const dist2 = Math.max(1, dist)
    
    const d = Math.min(dist2, Math.max(12, (dist2 / 4) - dist2))
    const D = dist2 * distance.value
    
    return [
      x + (d / D) * (position.x - x),
      y + (d / D) * (position.y - y)
    ]

  }

  const loop = () => {
    reset()
    draw()
    requestAnimationFrame(loop)
  }

  window.onresize = size

  canvas.onmousemove = (e) => {

    position.set({
      x: e.clientX - e.target.getBoundingClientRect().left,
      y: e.clientY - e.target.getBoundingClientRect().top
    }, 200)
  
  }
  
  canvas.onmouseenter = () => {
    
    distance.set({
      value: 1
    }, 200)
    
  }
  
  canvas.onmouseleave = () => {
    
    position.set({
      x: canvas.width / 2,
      y: canvas.height / 2
    }, 2000)
    
    distance.set({
      value: offset
    }, 12000)
    
  }

  loop()

})
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdn.jsdelivr.net/npm/ola