A button that sends a ripple out from exactly where it was pressed, or from the centre when pressed from the keyboard.
Customize
Usage
import { RippleButton } from "@/components/motionkit/ripple-button";
<RippleButton onClick={save}>Save changes</RippleButton>Motion
The exact curves this component runs, plotted with Motion's own maths.
Ripple
Press
Forms, dashboards and apps, anywhere a press should feel acknowledged: save, submit and add buttons.
Each press adds a circle centred on the press point, sized to reach the furthest corner, which scales up from zero while fading out, then removes itself when its animation completes. Presses from the keyboard report no pointer position, so they ripple from the centre. Ripples are independent, so quick presses overlap naturally.
Works in the App Router as is. Requires Tailwind CSS v4.
One command adds the file and installs its dependencies. Needs a components.json (run npx shadcn@latest init once if you don't have one).
npx shadcn@latest add https://www.motionkitui.com/r/ripple-button.jsonnpm install motionSave as components/motionkit/ripple-button.tsx.
"use client";
import { useState, type ComponentProps, type MouseEvent } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type RippleButtonProps = ComponentProps<"button"> & {
/** Colour of the ripple. */
rippleColor?: string;
};
type Ripple = { id: number; x: number; y: number; size: number };
/**
* A button that sends a ripple out from exactly where it was pressed, or from the centre when pressed with
* the keyboard. Quick presses stack ripples, and each one fades as it spreads.
*/
export function RippleButton({ rippleColor = "rgb(255 255 255 / 0.35)", className = "", children, onClick, ...props }: RippleButtonProps) {
const reduceMotion = useReducedMotion();
const [ripples, setRipples] = useState<Ripple[]>([]);
function handleClick(event: MouseEvent<HTMLButtonElement>) {
onClick?.(event);
if (reduceMotion) return;
const rect = event.currentTarget.getBoundingClientRect();
// a keyboard press reports no pointer position, so it ripples from the middle
const fromKeyboard = event.detail === 0;
const x = fromKeyboard ? rect.width / 2 : event.clientX - rect.left;
const y = fromKeyboard ? rect.height / 2 : event.clientY - rect.top;
// big enough to reach the furthest corner from the press
const size = Math.hypot(Math.max(x, rect.width - x), Math.max(y, rect.height - y)) * 2;
const id = performance.now();
setRipples((list) => [...list, { id, x, y, size }]);
}
return (
<button
{...props}
onClick={handleClick}
className={`relative isolate inline-flex items-center justify-center gap-2 overflow-hidden rounded-xl bg-[#0E7C86] px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-[#0B6770] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#5FAEB4] disabled:pointer-events-none disabled:opacity-50 ${className}`}
>
<AnimatePresence>
{ripples.map((ripple) => (
<motion.span
key={ripple.id}
aria-hidden
className="pointer-events-none absolute -z-10 rounded-full"
style={{ left: ripple.x - ripple.size / 2, top: ripple.y - ripple.size / 2, width: ripple.size, height: ripple.size, backgroundColor: rippleColor }}
initial={{ scale: 0, opacity: 1 }}
animate={{ scale: 1, opacity: 0 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
onAnimationComplete={() => setRipples((list) => list.filter((item) => item.id !== ripple.id))}
/>
))}
</AnimatePresence>
{children}
</button>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| rippleColor | string | "rgb(255 255 255 / 0.35)" | Colour of the ripple. |
| ...props | ButtonHTMLAttributes | — | Anything a <button> accepts. |
How we audit: the WCAG 2.2 animation checklist and respecting reduced motion in React.
Yes. The file starts with "use client", so you can import it straight into a Server Component page or layout. It works in Vite as is, and in Astro as a React island.
Only the motion package, beside React and Tailwind CSS v4. There is no MotionKit package and no Tailwind config to add.
Yes. Run npx shadcn@latest add https://www.motionkitui.com/r/ripple-button.json in any project with a components.json. It saves the file to components/motionkit and installs its dependencies.
With reduced motion on, no ripples are drawn.
Hold to Confirm
A button that fills while held, so destructive actions can never happen by accident.
Push Button
A chunky, tactile 3D button that lifts a little on hover and presses down onto its base when clicked, like a real key.
Magnetic Button
A button that leans toward the cursor with a springy, physical pull.