Buttons & Toggles

Ripple Button

A button with a spreading click ripple, radiating a Material-style wave out from the click point.

On click, a ripple radiates from the cursor's landing point, with a diameter reaching the farthest corner so the wave always covers the whole button. The ripple sits beneath the content and is clipped into the button's shape by overflow-hidden.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add ripple-button.en

New project or installation trouble? Start here →

Ripple Button is a React ripple effect button built with Motion and Tailwind CSS, with ripples mounted and removed through Motion's AnimatePresence. Rapid clicks each spawn their own ripple, and every ripple leaves the DOM once its animation ends.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

0.6
0.5
Active edited usageDefault props
<RippleButton/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • Primary actions such as form submit or add to cart, where a clear press response helps
  • Mobile web and PWAs that should feel like native touch feedback
  • Admin toolbars and dialog footers with confirm and cancel buttons
  • Not a good fit: buttons that need loading, success or error states; use Stateful Button instead

Installation

npx webberui@latest add https://webberui.com/r/ripple-button.en.json

Or, once registries are configured in components.json, install it as @webberui/ripple-button.

Usage

import { RippleButton } from "@/components/ui/ripple-button";

<RippleButton onClick={() => console.log("clicked")}>
  Click to ripple
</RippleButton>

To change the color just pass rippleColor; all other <button> attributes (type, disabled, onClick, and so on) are forwarded.

Examples

Light button with a dark ripple

The default ripple is translucent white, which disappears on a light background, so change rippleColor along with the button colours.

<RippleButton
  rippleColor="rgba(0, 0, 0, 0.12)"
  className="border border-neutral-200 bg-white text-neutral-900 hover:bg-neutral-50"
>
  Cancel
</RippleButton>

className is merged with the defaults through cn, so background, text colour and radius can all be overridden directly.

Form submit button

Native attributes such as type and disabled pass through to the <button>. The content wrapper is inline-flex gap-2, so an icon and label are spaced automatically.

<RippleButton type="submit" disabled={isSubmitting}>
  {isSubmitting && <Spinner className="size-4" />}
  {isSubmitting ? "Submitting" : "Place order"}
</RippleButton>

Spinner stands for the loading icon component in your project.

Brand colour with a slower ripple

duration is in seconds, and opacity is the ripple's starting opacity before it fades to 0.

<RippleButton
  rippleColor="rgba(255, 255, 255, 0.7)"
  duration={1}
  opacity={0.8}
  className="bg-indigo-600 text-white hover:bg-indigo-500"
>
  Create project
</RippleButton>

Props

Apart from the props below, all native <button> attributes are forwarded.

PropTypeDefaultDescription
rippleColorstring"rgba(255, 255, 255, 0.55)"Ripple color; any CSS color value
durationnumber0.6Duration of a single ripple's spread (seconds)
opacitynumber0.5Starting opacity of the ripple
classNamestring—Forwarded to the button; can override the default styles
childrenReactNode—Button content

Accessibility

  • When the user has "reduce motion" enabled at the system level, the ripple is skipped and only the ordinary button behavior remains
  • The ripple layer carries aria-hidden and pointer-events-none, so it affects neither assistive technology nor clicks
  • The native <button> semantics and keyboard focus ring are preserved, and interaction is disabled while disabled

FAQ

Does it work with the Next.js App Router?

Yes. The file already starts with "use client", so import it from a Server Component page as usual; event handlers such as onClick need to be passed from a Client Component.

Why doesn't the ripple spill outside the button?

Ripples sit in an overflow-hidden, rounded-[inherit] layer, so they are clipped to the button's shape. The diameter is twice the distance from the click point to the farthest corner, so a click anywhere covers the whole button.

It always renders a <button> and has no asChild-style option. To navigate, call your router's navigation method in onClick, or edit the source to render a link element.

Does my own onClick still run?

Yes. The component spawns the ripple first and then calls your onClick; style is also merged with the CSS variable it sets internally.

Does it respect reduced motion?

Yes. With "reduce motion" on, no ripples are created and the button behaves like a regular button.