Ripple
Add Ripple effect to everything — drop it inside any element for a smooth, cursor-tracking fill animation.
Demo
I'm a card
Hover to reveal the ripple filling the container from cursor position.
<div className="relative flex items-center gap-8">
<button id="myButton" className="px-6 py-2 group z-10 font-medium border-2 border-accent-demo cursor-pointer rounded-lg relative overflow-hidden">
<Ripple parent="#myButton" className="bg-accent-demo" />
<span className="relative z-10">I'm a button</span>
</button>
<div
ref={cardRef}
className="group relative w-64 rounded-2xl border border-[#2fc9da]/20 bg-[#2fc9da]/3 px-6 py-5 overflow-hidden cursor-pointer transition-shadow duration-500 hover:shadow-[0_0_24px_-4px_rgba(47,201,218,0.25)]"
>
<div className="absolute top-0 left-6 h-px w-12 bg-linear-to-r from-transparent via-[#2fc9da] to-transparent" />
<div className="relative z-10 flex flex-col gap-3">
<div className="flex items-center justify-between">
<span className="text-[11px] font-medium uppercase tracking-widest text-[#2fc9da] group-hover:text-white transition-colors duration-300">
Interactive
</span>
<span className="size-1.5 rounded-full bg-[#2fc9da] animate-pulse" />
</div>
<div>
<h3 className="font-serif text-lg leading-tight group-hover:text-white transition-colors duration-300">
I'm a card
</h3>
<p className="mt-1 text-sm text-muted-foreground mb-0 group-hover:text-white transition-colors duration-300">
Hover to reveal the ripple filling the container from cursor position.
</p>
</div>
<div className="flex items-center gap-1 text-xs text-muted-foreground group-hover:text-white transition-colors duration-300">
<span>Try it</span>
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="translate-x-0 group-hover:translate-x-1 transition-transform duration-300"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
</div>
</div>
<Ripple parent={cardRef} className="bg-[#2fc9da]/80 size-52" />
</div>
<Button ref={buttonRef} className="relative overflow-hidden group" >
<div className="z-20 relative">
Design System Button
</div>
<Ripple parent={buttonRef} className="bg-primary" />
</Button>
</div>Installation
npm install gsap @gsap/reactpnpm add gsap @gsap/reactyarn add gsap @gsap/reactbun add gsap @gsap/reactCopy and paste the following code into your project.
'use client';
import { useGSAP } from '@gsap/react';
import { useEffect, useRef, useState } from 'react';
import gsap from 'gsap';
import { cn } from '@/lib/utils';
interface RippleProps {
/** React ref or CSS selector string pointing to the parent element. */
parent: React.RefObject<HTMLElement | null> | string;
className?: string;
}
const isRef = (value: RippleProps['parent']): value is React.RefObject<HTMLElement | null> =>
typeof value !== 'string';
function Ripple({ parent, className }: RippleProps) {
const rippleRef = useRef<HTMLDivElement>(null);
const [isHovered, setIsHovered] = useState(false);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
useEffect(() => {
const el = isRef(parent) ? parent.current : document.querySelector<HTMLElement>(parent);
if (!el) return;
const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
const handleMouseMove = (e: MouseEvent) => {
const rect = el.getBoundingClientRect();
const offset = (rippleRef.current?.offsetWidth || 2) / 2;
setMousePosition({
x: e.clientX - rect.left - offset,
y: e.clientY - rect.top - offset,
});
};
el.addEventListener('mouseenter', handleMouseEnter);
el.addEventListener('mouseleave', handleMouseLeave);
el.addEventListener('mousemove', handleMouseMove);
return () => {
el.removeEventListener('mouseenter', handleMouseEnter);
el.removeEventListener('mouseleave', handleMouseLeave);
el.removeEventListener('mousemove', handleMouseMove);
};
}, [parent]);
useGSAP(() => {
if (!rippleRef.current) return;
const exitMultiplier = 1.2;
const calculatedPosition = {
x: mousePosition.x * (isHovered ? 1 : exitMultiplier),
y: mousePosition.y * (isHovered ? 1 : exitMultiplier),
};
gsap.timeline()
.to(rippleRef.current, {
x: mousePosition.x,
y: mousePosition.y,
duration: 0,
})
.to(rippleRef.current, {
scale: isHovered ? 2 : 0,
x: calculatedPosition.x,
y: calculatedPosition.y,
duration: 0.4,
ease: isHovered ? 'power1.out' : 'expo.out',
});
}, [isHovered, mousePosition]);
return (
<div
ref={rippleRef}
className={cn(
'absolute pointer-events-none rounded-full scale-0 top-0 left-0 size-28 z-1',
className
)}
/>
);
}
export { Ripple };Usage
import { Ripple } from "@/components/ripple";The Ripple component it's a standalone element you place inside any container. It listens to the parent element's mouse events and renders the ripple animation automatically. The parent can be referenced via a React ref or a CSS selector string.
With a CSS selector
The simplest approach. Give the parent element an id and pass the CSS selector as a string.
<button id="myButton" className="relative overflow-hidden">
<Ripple parent="#myButton" className="bg-white" />
<span className="relative z-10">Click me</span>
</button>With a React ref
For a more React-idiomatic approach, pass a ref to the parent element.
const buttonRef = useRef<HTMLButtonElement>(null);
<button ref={buttonRef} className="relative overflow-hidden">
<Ripple parent={buttonRef} className="bg-white" />
<span className="relative z-10">Click me</span>
</button>On any element
Since Ripple attaches to any parent, you can use it on cards, divs, links — anything with relative and overflow-hidden.
const cardRef = useRef<HTMLDivElement>(null);
<div ref={cardRef} className="relative overflow-hidden rounded-2xl p-6">
<Ripple parent={cardRef} className="bg-cyan-500/80 size-52" />
<div className="relative z-10">
<h3>I'm a card</h3>
<p>Hover to reveal the ripple.</p>
</div>
</div>Styling the ripple
Control the ripple style through the className prop.
<Ripple parent={ref} className="bg-white size-10 sm:size-20 md:size-32 lg:size-46" />With Design System
You can drop Ripple inside any design system button (e.g., Shadcn UI) — just ensure the button has relative and overflow-hidden.
import { Button } from "@/components/ui/button";
import { Ripple } from "@/components/ripple";
const buttonRef = useRef<HTMLButtonElement>(null);
<Button ref={buttonRef} className="relative overflow-hidden group">
<div className="z-20 relative">
Design System Button
</div>
<Ripple parent={buttonRef} className="bg-primary" />
</Button>Requirements
For the ripple to render correctly, the parent element must have relative and overflow-hidden.
Also, the content shown on top of the ripple must have a z-index value bigger than 1.
Props
| Prop | Type | Default Value | Description |
|---|---|---|---|
| parent | React.RefObject<HTMLElement | null> | string | - | React ref or CSS selector pointing to the parent element |
| className? | string | - | Class name applied to the ripple div |