bundui svg logo
Bundui

Ripple Effect

Ripple effect is a modern animation component designed to enrich user interaction by creating a dynamic ripple effect when clicking on buttons or any HTML element. Built with Tailwind CSS and Motion.

import { RippleEffect } from "@/components/core/ripple-effect";
import { Button } from "@/components/ui/button";

export default function RippleEffectButton() {
  return (
    <RippleEffect className="rounded-lg">
      <Button size="lg">Click me</Button>
    </RippleEffect>
  );
}

Installation

Install the following dependencies:
npm install motion
Copy and paste the following code into your project:
"use client";

import React from "react";
import { motion, AnimatePresence } from "motion/react";

interface RippleProps {
  children: React.ReactNode;
  color?: string;
  duration?: number;
  className?: string;
}

interface RipplePosition {
  x: number;
  y: number;
  size: number;
  id: number;
}

export const RippleEffect = ({
  children,
  color = "rgba(255, 255, 255, 0.7)",
  duration = 0.8,
  className,
}: RippleProps) => {
  const [ripples, setRipples] = React.useState<RipplePosition[]>([]);
  const containerRef = React.useRef<HTMLDivElement>(null);

  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
    if (!containerRef.current) return;

    const rect = containerRef.current.getBoundingClientRect();

    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    const size = Math.max(rect.width, rect.height);

    const newRipple: RipplePosition = {
      x,
      y,
      size: size * 2,
      id: Date.now(),
    };

    setRipples((prevRipples) => [...prevRipples, newRipple]);

    setTimeout(() => {
      setRipples((prevRipples) =>
        prevRipples.filter((ripple) => ripple.id !== newRipple.id),
      );
    }, duration * 1000);
  };

  return (
    <div
      ref={containerRef}
      className={`inline-flex relative overflow-hidden rounded-lg ${className}`}
      onMouseDown={handleClick}
    >
      {children}
      <AnimatePresence>
        {ripples.map((ripple) => (
          <motion.div
            key={ripple.id}
            initial={{
              width: 0,
              height: 0,
              x: ripple.x,
              y: ripple.y,
              opacity: 0.5,
              borderRadius: "100%",
            }}
            animate={{
              width: ripple.size,
              height: ripple.size,
              x: ripple.x - ripple.size / 2,
              y: ripple.y - ripple.size / 2,
              opacity: 0,
              borderRadius: "100%",
            }}
            exit={{ opacity: 0 }}
            transition={{ duration, ease: "easeOut" }}
            style={{
              position: "absolute",
              backgroundColor: color,
              pointerEvents: "none",
            }}
          />
        ))}
      </AnimatePresence>
    </div>
  );
};
Update the import paths to match your project setup.

Usage

import { RippleEffect } from "@/components/core/ripple-effect";
import { Button } from "@/components/ui/button";

export default function RippleEffectButton() {
  return (
    <RippleEffect className="rounded-lg">
      <Button size="lg">Click me</Button>
    </RippleEffect>
  );
}

Examples

Colored

Blue
Green
Purple
Red
import { RippleEffect } from "@/components/core/ripple-effect";

export default function RippleEffectColored() {
  return (
    <div className="grid grid-cols-2 gap-4">
      <div>
        <RippleEffect className="block" color="var(--color-blue-400)">
          <div className="w-36 bg-blue-200 p-2 text-center text-blue-800">Blue</div>
        </RippleEffect>
      </div>
      <div>
        <RippleEffect className="block" color="var(--color-green-400)">
          <div className="w-36 bg-green-200 p-2 text-center text-green-800">Green</div>
        </RippleEffect>
      </div>
      <div>
        <RippleEffect className="block" color="var(--color-purple-400)">
          <div className="w-36 bg-purple-200 p-2 text-center text-purple-800">Purple</div>
        </RippleEffect>
      </div>
      <div>
        <RippleEffect className="block" color="var(--color-red-400)">
          <div className="w-36 bg-red-200 p-2 text-center text-red-800">Red</div>
        </RippleEffect>
      </div>
    </div>
  );
}

Image

ripple effect image
ripple effect image
import Image from "next/image";

import { RippleEffect } from "@/components/core/ripple-effect";

export default function RippleEffectImage() {
  return (
    <div className="flex gap-4">
      <RippleEffect color="var(--color-black)">
        <figure className="relative aspect-square w-40">
          <Image
            src="https://bundui-images.netlify.app/products/06.jpeg"
            fill
            alt="ripple effect image"
          />
        </figure>
      </RippleEffect>
      <RippleEffect color="var(--color-purple-600)">
        <figure className="relative aspect-square w-40">
          <Image
            src="https://bundui-images.netlify.app/products/07.jpeg"
            fill
            alt="ripple effect image"
          />
        </figure>
      </RippleEffect>
    </div>
  );
}

Props

PropTypeDefault
className?
string
-
duration?
number
0.8
color?
string
"rgba(255, 255, 255, 0.7)"
children
React.ReactNode
-