Skip to content

Ripple Click Effect

A Material-style ripple that expands from the exact click point.

Freeplayfulbrutalist
Category
Components
Added
2026-08-22
Deps
1
Updated
2026-09-01
Preview

Props

Requires framer-motion
"use client";

import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";

let rippleId = 0;

export function RippleButton({
  label = "Click me",
  color = "light",
}: {
  label?: string;
  color?: "light" | "dark";
}) {
  const [ripples, setRipples] = useState<{ id: number; x: number; y: number; size: number }[]>([]);

  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    const rect = e.currentTarget.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const size = Math.hypot(Math.max(x, rect.width - x), Math.max(y, rect.height - y)) * 2;
    const id = rippleId++;
    setRipples((prev) => [...prev, { id, x, y, size }]);
    setTimeout(() => setRipples((prev) => prev.filter((r) => r.id !== id)), 700);
  }

  return (
    <button
      onClick={handleClick}
      className="relative overflow-hidden rounded-xl bg-red-500 px-8 py-4 font-semibold text-white"
    >
      {label}
      <AnimatePresence>
        {ripples.map((r) => (
          <motion.span
            key={r.id}
            className="pointer-events-none absolute rounded-full"
            style={{
              left: r.x,
              top: r.y,
              width: r.size,
              height: r.size,
              marginLeft: -r.size / 2,
              marginTop: -r.size / 2,
              background: color === "light" ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.35)",
            }}
            initial={{ scale: 0, opacity: 0.6 }}
            animate={{ scale: 1, opacity: 0 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.7, ease: "easeOut" }}
          />
        ))}
      </AnimatePresence>
    </button>
  );
}
Notes

About this component

A ripple that always starts from a button's center looks fake the moment someone clicks near a corner; a real Material ripple originates exactly where the pointer landed. This computes the click point relative to the button via getBoundingClientRect, then sizes the ripple as twice the distance from that point to the farthest corner, which is what guarantees full coverage regardless of where the click happened. Sizing off the button's average dimensions instead would leave a visible sliver uncovered on an off-center click. Each ripple is tracked as its own object in an array with a unique id, so rapid repeated clicks spawn independent overlapping ripples rather than one ripple restarting and truncating the previous click's animation, a common failure mode when a single ripple element gets reused. AnimatePresence handles the exit animation once a matching setTimeout prunes the ripple from state. The button needs both overflow-hidden and relative positioning for the ripple to clip to its rounded corners instead of spilling past them, which is easy to lose if you copy just the ripple logic into an existing button.

Curator’s note

Sizing the ripple off the click point's distance to the farthest corner (not the button's average size) is what stops a corner-click from leaving an uncovered sliver — easy to miss until you actually click near an edge.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

Trending

A button that scatters a quick burst of confetti particles on click.

buttonconfetticlick
Featured

Magnetic Button

components

A button whose label leans toward the cursor with a springy pull.

buttoncursorspring

A share icon that expands into a row of social options with a copy-link state.

sharebuttonexpand