Buttons
RippleCore
Every press sends a ripple out from the exact point you touched. Enter and Space ripple from the centre.
Download
- React
- Tailwind CSS v4
- motion
components/ui/ripple-core.tsx
"use client";
import { useRef, useState, type ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type RippleCoreProps = {
children: ReactNode;
className?: string;
onClick?: () => void;
/** Colour of the ripple. */
color?: string;
/** Seconds one ripple lasts. */
duration?: number;
};
type Ripple = { id: number; x: number; y: number; r: number };
/* RippleCore: every press sends a ripple out from the exact point you touched. Keyboard presses ripple from the centre. */
export function RippleCore({
children,
className = "rounded-xl bg-[#a78bfa] px-9 py-4 text-[15px] font-medium text-[#1a1530]",
onClick,
color = "rgba(255,255,255,0.55)",
duration = 0.6,
}: RippleCoreProps) {
const reduce = useReducedMotion();
const [ripples, setRipples] = useState<Ripple[]>([]);
const next = useRef(0);
const add = (el: HTMLElement, x: number, y: number) => {
if (reduce) return;
const { width: w, height: h } = el.getBoundingClientRect();
const r = Math.hypot(Math.max(x, w - x), Math.max(y, h - y));
setRipples((rs) => [...rs, { id: next.current++, x, y, r }]);
};
return (
<button
type="button"
onPointerDown={(e) => {
const b = e.currentTarget.getBoundingClientRect();
add(e.currentTarget, e.clientX - b.left, e.clientY - b.top);
}}
onClick={(e) => {
// detail 0 = Enter or Space, not a pointer.
if (e.detail === 0) {
const b = e.currentTarget.getBoundingClientRect();
add(e.currentTarget, b.width / 2, b.height / 2);
}
onClick?.();
}}
className={`relative isolate overflow-hidden ${className}`}
>
{ripples.map((p) => (
<motion.span
key={p.id}
aria-hidden
className="pointer-events-none absolute -z-10 rounded-full"
style={{ left: p.x - p.r, top: p.y - p.r, width: p.r * 2, height: p.r * 2, background: color }}
initial={{ scale: 0, opacity: 1 }}
animate={{ scale: 1, opacity: 0 }}
transition={{ duration, ease: [0.22, 1, 0.36, 1] }}
onAnimationComplete={() => setRipples((rs) => rs.filter((r) => r.id !== p.id))}
/>
))}
<span className="relative">{children}</span>
</button>
);
}
1. Install the dependency.
npm i motion2. Paste the Code tab into
components/ui/ripple-core.tsx. The styles are Tailwind CSS v4 classes.3. Use it. This is the preview above:
app/ripple-core-demo.tsx
import { RippleCore } from "@/components/ui/ripple-core"; export default function RippleCoreExample() { return ( <div className="grid h-full place-items-center bg-[radial-gradient(circle_at_50%_60%,#1d1a33,#0d0c14_70%)]"> <RippleCore>Tap anywhere on me</RippleCore> </div> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| children | ReactNode | — | Button label. |
| onClick | () => void | — | Click handler. |
| color | string | "rgba(255,255,255,0.55)" | Colour of the ripple. |
| duration | number | 0.6 | Seconds one ripple lasts. |
| className | string | violet block | Replaces the default look. |