Back to Dictionary
Action Feedback
●○○Ready to Use
Recommended defaultRipple Effect
A circular wave that expands from the point of contact when a user clicks or taps an element, providing immediate spatial feedback.
Use
Text buttons and filled buttons
Avoid
Small iconic buttons where the ripple obscures the icon
Safer Alternative
No safer default listed.
Risk Level
Low risk
Live Demo
What It Is
A circular wave that expands from the point of contact when a user clicks or taps an element, providing immediate spatial feedback.
✓When to Use
- Text buttons and filled buttons
- List items in a mobile app
- Card surfaces that are clickable
✕When NOT to Use
- Small iconic buttons where the ripple obscures the icon
- Links inline within text paragraphs
Configuration Tips
- 01Ensure the ripple color has low opacity (e.g., black or white at 10-20% opacity)
- 02Clip the ripple to the container with overflow-hidden
You've Seen It In
Google/Material DesignAndroid OSYouTube
AI Implementation Prompts
Move from a fast scaffold to production details, then tune timing and edge states.
Add a material design ripple effect to this button that starts where I click.
Reference Implementation
The same implementation approach used by the live preview, kept compact enough to inspect and adapt.
'use client';
import { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
export function RippleEffectDemo({ isPlaying = false }: { isPlaying?: boolean }) {
const [ripples, setRipples] = useState<{ x: number; y: number; id: number }[]>([]);
const buttonRef = useRef<HTMLButtonElement>(null);
const addRipple = (x: number, y: number) => {
const newRipple = { x, y, id: Date.now() + Math.random() };
setRipples((prev) => [...prev, newRipple]);
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!buttonRef.current) return;
const rect = buttonRef.current.getBoundingClientRect();
addRipple(e.clientX - rect.left, e.clientY - rect.top);
};
useEffect(() => {
if (!isPlaying || !buttonRef.current) return;
let timeout: NodeJS.Timeout;
const triggerRandomRipple = () => {
if (buttonRef.current) {
const { width, height } = buttonRef.current.getBoundingClientRect();
"text-stone-400 italic">// Random position within button
const x = Math.random() * width;
const y = Math.random() * height;
addRipple(x, y);
}
timeout = setTimeout(triggerRandomRipple, 1200);
};
triggerRandomRipple();
return () => clearTimeout(timeout);
}, [isPlaying]);
const handleAnimationComplete = (id: number) => {
setRipples((prev) => prev.filter((r) => r.id !== id));
};
return (
<div className="flex h-64 w-full items-center justify-center rounded-2xl bg-white p-6 shadow-sm ring-1 ring-stone-200">
<button
ref={buttonRef}
onClick={handleClick}
className="relative overflow-hidden min-w-[160px] rounded-xl bg-stone-900 px-10 py-4 text-base font-medium text-white shadow-sm transition-colors hover:bg-stone-800"
>
<div className="relative z-10 flex items-center justify-center gap-2">
Tap anywhere
</div>
<AnimatePresence>
{ripples.map((ripple) => (
<motion.span
key={ripple.id}
initial={{ scale: 0, opacity: 0.4 }}
animate={{ scale: 4, opacity: 0 }}
transition={{ duration: 0.6, ease: 'easeOut' }}
onAnimationComplete={() => handleAnimationComplete(ripple.id)}
className="absolute z-0 rounded-full bg-white/30"
style={{
left: ripple.x,
top: ripple.y,
width: 100,
height: 100,
marginTop: -50,
marginLeft: -50,
pointerEvents: 'none',
}}
/>
))}
</AnimatePresence>
</button>
</div>
);
}