Ripple Button

A ripple spreads out from exactly where you click.

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/ripple-button.tsx

    components/ui/ripple-button.tsx
    "use client";
    
    import { motion } from "motion/react";
    import { useState, type ButtonHTMLAttributes, type MouseEvent } from "react";
    
    type Ripple = { id: number; x: number; y: number; size: number };
    
    type RippleButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
      rippleColor?: string;
    };
    
    export function RippleButton({ children, className = "", rippleColor = "rgba(255,255,255,0.45)", onClick, ...props }: RippleButtonProps) {
      const [ripples, setRipples] = useState<Ripple[]>([]);
    
      function handleClick(e: MouseEvent<HTMLButtonElement>) {
        const rect = e.currentTarget.getBoundingClientRect();
        const size = Math.max(rect.width, rect.height) * 2.2;
        const ripple = { id: Date.now() + Math.random(), x: e.clientX - rect.left - size / 2, y: e.clientY - rect.top - size / 2, size };
        setRipples((r) => [...r, ripple]);
        onClick?.(e);
      }
    
      return (
        <button
          onClick={handleClick}
          className={`relative inline-flex items-center justify-center gap-2 overflow-hidden rounded-xl bg-blue-600 px-6 py-3 text-sm font-medium text-white shadow-lg shadow-blue-600/25 transition-colors hover:bg-blue-700 ${className}`}
          {...props}
        >
          {ripples.map((r) => (
            <motion.span
              key={r.id}
              aria-hidden
              className="pointer-events-none absolute rounded-full"
              style={{ left: r.x, top: r.y, width: r.size, height: r.size, background: rippleColor }}
              initial={{ scale: 0, opacity: 0.7 }}
              animate={{ scale: 1, opacity: 0 }}
              transition={{ duration: 0.75, ease: "easeOut" }}
              onAnimationComplete={() => setRipples((all) => all.filter((x) => x.id !== r.id))}
            />
          ))}
          <span className="relative">{children}</span>
        </button>
      );
    }
    
    export default function Demo() {
      return (
        <div className="flex flex-wrap items-center justify-center gap-4">
          <RippleButton>Click anywhere on me</RippleButton>
          <RippleButton rippleColor="rgba(37,99,235,0.18)" className="!bg-white !text-zinc-900 !shadow-sm ring-1 ring-zinc-200 hover:!bg-zinc-50">
            Secondary ripple
          </RippleButton>
        </div>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.