Bubbly Button

Button with bubble animation

Installation

Install Dependencies

npm i clsx tailwind-merge framer-motion

Create @/utils/cn.ts file

import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Bubbly Button Component

"use client";

import { cn } from "@/lib/utils";
import React, { useEffect } from "react";

interface BubblyButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  children: string;
  color?: string;
  onClick?: () => void;
}

const BubblyButton: React.FC<BubblyButtonProps> = ({ children, className, onClick, color = "#0096FF" }) => {
  const animateButton = function (e: any) {
    e.preventDefault;
    e.target.classList.remove("animate");

    e.target.classList.add("animate");
    setTimeout(function () {
      e.target.classList.remove("animate");
    }, 700);
  };

  useEffect(() => {
    const bubblyButtons = document.getElementsByClassName("bubbly-button");

    for (let i = 0; i < bubblyButtons.length; i++) {
      bubblyButtons[i].addEventListener("click", animateButton, false);
    }

    return () => {
      for (let i = 0; i < bubblyButtons.length; i++) {
        bubblyButtons[i].removeEventListener("click", animateButton, false);
      }
    };
  }, []);

  return (
    <div>
      <button
        className={cn(
          `bubbly-button relative transform rounded border-none px-6 py-4 text-white transition-transform duration-100 ease-in active:scale-90`,
          className
        )}
        onClick={onClick}
        style={{
          backgroundColor: color,
          boxShadow: `0 2px 25px ${color}`
        }}
      >
        {children}
      </button>

      <style jsx>{`
        .bubbly-button {
          &:focus {
            outline: 0;
          }

          &:before,
          &:after {
            position: absolute;
            content: "";
            display: block;
            width: 140%;
            height: 100%;
            left: -20%;
            z-index: -1000;
            transition: all ease-in-out 0.5s;
            background-repeat: no-repeat;
          }

          &:before {
            display: none;
            top: -75%;
            background-image: radial-gradient(circle, ${color} 20%, transparent 20%),
              radial-gradient(circle, transparent 20%, ${color} 20%, transparent 30%),
              radial-gradient(circle, ${color} 20%, transparent 20%), radial-gradient(circle, ${color} 20%, transparent 20%),
              radial-gradient(circle, transparent 10%, ${color} 15%, transparent 20%),
              radial-gradient(circle, ${color} 20%, transparent 20%), radial-gradient(circle, ${color} 20%, transparent 20%),
              radial-gradient(circle, ${color} 20%, transparent 20%), radial-gradient(circle, ${color} 20%, transparent 20%);
            background-size:
              10% 10%,
              20% 20%,
              15% 15%,
              20% 20%,
              18% 18%,
              10% 10%,
              15% 15%,
              10% 10%,
              18% 18%;
            //background-position: 0% 80%, -5% 20%, 10% 40%, 20% 0%, 30% 30%, 22% 50%, 50% 50%, 65% 20%, 85% 30%;
          }

          &:after {
            display: none;
            bottom: -75%;
            background-image: radial-gradient(circle, ${color} 20%, transparent 20%), radial-gradient(circle, ${color} 20%, transparent 20%),
              radial-gradient(circle, transparent 10%, ${color} 15%, transparent 20%),
              radial-gradient(circle, ${color} 20%, transparent 20%), radial-gradient(circle, ${color} 20%, transparent 20%),
              radial-gradient(circle, ${color} 20%, transparent 20%), radial-gradient(circle, ${color} 20%, transparent 20%);
            background-size:
              15% 15%,
              20% 20%,
              18% 18%,
              20% 20%,
              15% 15%,
              10% 10%,
              20% 20%;
            //background-position: 5% 90%, 10% 90%, 10% 90%, 15% 90%, 25% 90%, 25% 90%, 40% 90%, 55% 90%, 70% 90%;
          }

          &:active {
            transform: scale(0.9);
            background-color: darken(${color}, 5%);
            box-shadow: 0 2px 25px rgba(255, 0, 130, 0.2);
          }

          &.animate {
            &:before {
              display: block;
              animation: topBubbles ease-in-out 0.75s forwards;
            }
            &:after {
              display: block;
              animation: bottomBubbles ease-in-out 0.75s forwards;
            }
          }
        }

        @keyframes topBubbles {
          0% {
            background-position:
              5% 90%,
              10% 90%,
              10% 90%,
              15% 90%,
              25% 90%,
              25% 90%,
              40% 90%,
              55% 90%,
              70% 90%;
          }
          50% {
            background-position:
              0% 80%,
              0% 20%,
              10% 40%,
              20% 0%,
              30% 30%,
              22% 50%,
              50% 50%,
              65% 20%,
              90% 30%;
          }
          100% {
            background-position:
              0% 70%,
              0% 10%,
              10% 30%,
              20% -10%,
              30% 20%,
              22% 40%,
              50% 40%,
              65% 10%,
              90% 20%;
            background-size:
              0% 0%,
              0% 0%,
              0% 0%,
              0% 0%,
              0% 0%,
              0% 0%;
          }
        }

        @keyframes bottomBubbles {
          0% {
            background-position:
              10% -10%,
              30% 10%,
              55% -10%,
              70% -10%,
              85% -10%,
              70% -10%,
              70% 0%;
          }
          50% {
            background-position:
              0% 80%,
              20% 80%,
              45% 60%,
              60% 100%,
              75% 70%,
              95% 60%,
              105% 0%;
          }
          100% {
            background-position:
              0% 90%,
              20% 90%,
              45% 70%,
              60% 110%,
              75% 80%,
              95% 70%,
              110% 10%;
            background-size:
              0% 0%,
              0% 0%,
              0% 0%,
              0% 0%,
              0% 0%,
              0% 0%;
          }
        }
      `}</style>
    </div>
  );
};

export default BubblyButton;