<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