Ripple Button

PreviousNext

Button with touch-aware ripple effect on click

Docs
animbitscomponent

Preview

Loading preview…
registry/new-york/animations/buttons/ripple.tsx
"use client";
import * as React from "react";
import { motion, HTMLMotionProps } from "motion/react";
import { cn } from "@/lib/utils";
interface RippleButtonProps extends Omit<HTMLMotionProps<"button">, "onClick"> {
  children: React.ReactNode;
  rippleColor?: string;
  duration?: number;
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}
interface Ripple {
  x: number;
  y: number;
  id: number;
}
export function RippleButton({
  children,
  className,
  rippleColor = "rgba(255, 255, 255, 0.6)",
  duration = 0.6,
  onClick,
  ...props
}: RippleButtonProps) {
  const [ripples, setRipples] = React.useState<Ripple[]>([]);
  const buttonRef = React.useRef<HTMLButtonElement>(null);
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    const button = buttonRef.current;
    if (!button) return;
    const rect = button.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const newRipple = {
      x,
      y,
      id: Date.now(),
    };
    setRipples((prev) => [...prev, newRipple]);
    setTimeout(() => {
      setRipples((prev) => prev.filter((r) => r.id !== newRipple.id));
    }, duration * 1000);
    onClick?.(e);
  };
  return (
    <motion.button
      ref={buttonRef}
      className={cn("relative overflow-hidden", className)}
      onClick={handleClick}
      {...props}
    >
      {children}
      {ripples.map((ripple) => (
        <motion.span
          key={ripple.id}
          className="absolute rounded-full pointer-events-none"
          style={{
            left: ripple.x,
            top: ripple.y,
            backgroundColor: rippleColor,
          }}
          initial={{ width: 0, height: 0, opacity: 1 }}
          animate={{
            width: 500,
            height: 500,
            opacity: 0,
            x: -250,
            y: -250,
          }}
          transition={{ duration, ease: "easeOut" }}
        />
      ))}
    </motion.button>
  );
}

Installation

npx shadcn@latest add @animbits/buttons-ripple

Usage

import { ButtonsRipple } from "@/components/buttons-ripple"
<ButtonsRipple />