Overview
A button ripple draws a soft circle that expands from the click point outward, fading as it grows. The pattern uses a radial gradient on a pseudo-element with origin coordinates passed in as CSS custom properties from a pointer event, so the ripple actually starts at the cursor — not the button center.
When to use it
Reach for ripples on Material-leaning button systems and anywhere a click needs tactile confirmation (submit buttons, toolbar actions, list-item taps). Skip it for inline-text links, tiny icon-only buttons, or any control where the ripple radius would extend past the click target itself. Skip it for buttons that fire rapidly (volume +/-, counter steppers) — rapid clicks pile ripples on top of each other and the effect becomes noise.
How it works
On pointerdown read the click coordinates, subtract the button’s getBoundingClientRect() origin, and write the results into --ripple-x and --ripple-y CSS custom properties on the button itself. A ::after pseudo-element listens for those variables and renders a radial-gradient(circle at var(--ripple-x) var(--ripple-y), ...) that animates from zero radius to past the longest button diagonal. The pseudo-element sits inside an overflow: hidden button so the ripple clips to the button’s rounded corners. A second class .is-rippling toggles on the same pointerdown to retrigger the keyframe; remove it on animationend so the next click starts clean.
Production gotchas
Rapid double-clicks stack two ripples on top of each other; either debounce the trigger to one ripple per animation-duration window or render a queue of pseudo-element clones if you want every click acknowledged. Forgetting overflow: hidden on the button leaks the ripple past the rounded corner and the effect looks amateur. The pseudo-element needs pointer-events: none or it intercepts clicks meant for the button label. Buttons rendered inside a flex/grid container with min-width: 0 can produce zero-width getBoundingClientRect() measurements on the first paint — defer the listener wire-up to requestAnimationFrame after mount.
Accessibility
The ripple is purely decorative — the click event already fires the action; screen readers ignore the pseudo-element. Under prefers-reduced-motion: reduce skip the radius animation entirely and use a flat 60ms opacity fade instead, so the click still has tactile feedback without the outward expansion. Verify the ripple color has at least 3:1 contrast against the button surface or it disappears for low-vision users.
References
Implementation depth
The ripple is press feedback, not the button action itself. Track pointer coordinates into CSS variables for pointer users, but keep the same button state and label for keyboard and screen-reader users.
Use overflow hidden and a bounded radial-gradient so the effect does not bleed outside rounded corners. Reduced motion can switch the ripple to an instant background flash while preserving click acknowledgement.