Random Access Components

Ripple

Add Ripple effect to everything — drop it inside any element for a smooth, cursor-tracking fill animation.

TailwindCSS iconTailwind CSS
React IconReact
Next.js IconNext.js
Gsap iconGSAP

Demo

Interactive

I'm a card

Hover to reveal the ripple filling the container from cursor position.

Try itArrow right icon
<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&apos;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&apos;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/react
pnpm add gsap @gsap/react
yarn add gsap @gsap/react
bun add gsap @gsap/react

Copy and paste the following code into your project.

ripple.tsx
'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

PropTypeDefault ValueDescription
parentReact.RefObject<HTMLElement | null> | string-React ref or CSS selector pointing to the parent element
className?string-Class name applied to the ripple div

On this page