Hover Glow Button

Glow effect button on hover

Installation

Install Dependencies

npm i clsx tailwind-merge framer-motion
npm i chroma-js
npm i gsap

Create @/utils/cn.ts file

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

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

Hover Glow Button Component

"use client";

import chroma from "chroma-js";
import gsap from "gsap";
import React, { RefObject, useEffect, useRef } from "react";

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

const HoverGlowButton: React.FC<HoverGlowButtonProps> = ({ children, className, onClick }) => {
  const buttonRef: RefObject<HTMLButtonElement> = useRef(null);

  useEffect(() => {
    const button = buttonRef.current;

    if (button) {
      let gradientElem = button.querySelector(".gradient");

      if (!gradientElem) {
        gradientElem = document.createElement("div");
        gradientElem.classList.add("gradient");
        button.appendChild(gradientElem);
      }

      const handlePointerMove = (e: { clientX: number; clientY: number }) => {
        const rect = button.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        gsap.to(button, {
          "--pointer-x": `${x}px`,
          "--pointer-y": `${y}px`,
          "duration": 0.6
        });

        gsap.to(button, {
          "--button-glow": chroma
            .mix(
              getComputedStyle(button).getPropertyValue("--button-glow-start").trim(),
              getComputedStyle(button).getPropertyValue("--button-glow-end").trim(),
              x / rect.width
            )
            .hex(),
          "duration": 0.2
        });
      };

      button.addEventListener("pointermove", handlePointerMove);

      return () => {
        // Cleanup listener on unmount
        button.removeEventListener("pointermove", handlePointerMove);
      };
    }
  }, []);

  return (
    <div>
      <button
        ref={buttonRef}
        className={`glow-button relative rounded-lg bg-gray-800 p-4 font-bold text-white ${className}`}
        style={{
          ["--button-glow-start" as string]: "#ff5f6d",
          ["--button-glow-end" as string]: "#ffc371",
          ["--button-glow" as string]: "#ff5f6d"
        }}
        onClick={onClick}
      >
        <div className="gradient pointer-events-none absolute inset-0"></div>
        <span>{children}</span>
      </button>

      <style jsx>{`
        .glow-button {
          --button-background: #09041e;
          --button-color: #fff;
          --button-shadow: rgba(33, 4, 104, 0.2);
          --button-shine-left: rgba(120, 0, 245, 0.5);
          --button-shine-right: rgba(200, 148, 255, 0.65);
          --button-glow-start: #b000e8;
          --button-glow-end: #009ffd;

          appearance: none;
          outline: none;
          border: none;
          font-family: inherit;
          font-size: 16px;
          font-weight: 500;
          border-radius: 11px;
          position: relative;
          line-height: 24px;
          cursor: pointer;
          color: var(--button-color);
          padding: 0;
          margin: 0;
          background: none;
          z-index: 1;
          box-shadow: 0 8px 20px var(--button-shadow);

          .gradient {
            position: absolute;
            inset: 0;
            border-radius: inherit;
            overflow: hidden;
            -webkit-mask-image: -webkit-radial-gradient(white, black);
            transform: scaleY(1.02) scaleX(1.005) rotate(-0.35deg);

            &:before {
              content: "";
              position: absolute;
              top: 0;
              left: 0;
              right: 0;
              transform: scale(1.05) translateY(-44px) rotate(0deg) translateZ(0);
              padding-bottom: 100%;
              border-radius: 50%;
              background: linear-gradient(90deg, var(--button-shine-left), var(--button-shine-right));
              animation: rotate linear 2s infinite;
            }
          }

          span {
            z-index: 1;
            position: relative;
            display: block;
            padding: 10px 28px;
            box-sizing: border-box;
            width: fit-content;
            min-width: 124px;
            border-radius: inherit;
            background-color: var(--button-background);
            overflow: hidden;
            -webkit-mask-image: -webkit-radial-gradient(white, black);

            &:before {
              content: "";
              position: absolute;
              left: -16px;
              top: -16px;
              transform: translate(var(--pointer-x, 0px), var(--pointer-y, 0px)) translateZ(0);
              width: 32px;
              height: 32px;
              border-radius: 50%;
              background-color: var(--button-glow, transparent);
              opacity: var(--button-glow-opacity, 0);
              transition: opacity var(--button-glow-duration, 0.5s);
              filter: blur(20px);
            }
          }

          &:hover {
            --button-glow-opacity: 1;
            --button-glow-duration: 0.25s;
          }
        }

        @keyframes rotate {
          to {
            transform: scale(1.05) translateY(-44px) rotate(360deg) translateZ(0);
          }
        }

        html {
          box-sizing: border-box;
          -webkit-font-smoothing: antialiased;
        }

        * {
          box-sizing: inherit;
          &:before,
          &:after {
            box-sizing: inherit;
          }
        }

        body {
          min-height: 100vh;
          display: flex;
          font-family: "Inter", Arial;
          justify-content: center;
          align-items: center;
          background-color: #020112;
          overflow: hidden;

          &:before {
            content: "";
            position: absolute;
            inset: 40% -60% 0 -60%;
            background-image: radial-gradient(ellipse at bottom, #1d0559 0%, #020112 50%);
            opacity: 0.4;
          }
        }
      `}</style>
    </div>
  );
};

export default HoverGlowButton;