---
title: Ripple
description: Quiet radial press feedback.
group: Pointer
order: 26
new: true
---

A soft ink wave from the press point. Wrap a button or any surface and tune duration, opacity, and color.

## Button

```tsx
import { Ripple } from "@/components/motion/ripple"
import { Button } from "@/components/ui/button"

export function Example() {
  return (
    <Ripple className="inline-flex rounded-md">
      <Button>Press me</Button>
    </Ripple>
  )
}
```

## Surface

```tsx
import { Ripple } from "@/components/motion/ripple"
import { Button } from "@/components/ui/button"

export function Example() {
  return (
    <Ripple className="inline-flex rounded-md">
      <Button>Press me</Button>
    </Ripple>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add ripple
```

## Usage

```tsx
import { Ripple } from "@/components/motion/ripple"
import { Button } from "@/components/ui/button"
```

```tsx
<Ripple className="inline-flex rounded-md">
  <Button>Press me</Button>
</Ripple>
```

## API Reference

### Ripple

| Prop | Type | Default |
| --- | --- | --- |
| duration | number | 650 |
| color | string | currentColor |
| opacity | number | 0.28 |
| centered | boolean | false |
| className | string | — |
| children | ReactNode | — |

Also accepts the other native `div` attributes. Duration is in milliseconds.

## Source

```tsx
"use client";

import "@/lib/motion-styles";

import {
  useCallback,
  useRef,
  useState,
  type ComponentProps,
  type CSSProperties,
  type PointerEvent as ReactPointerEvent,
  type ReactNode,
} from "react";
import { cn } from "@/lib/utils";

type RippleMark = {
  id: number;
  x: number;
  y: number;
  size: number;
};

export type RippleProps = ComponentProps<"div"> & {
  /** Expand + fade length in ms. */
  duration?: number;
  /** Ink color for the wave. */
  color?: string;
  /** Peak opacity of the wave (0–1). */
  opacity?: number;
  /** Always start from the center instead of the press point. */
  centered?: boolean;
  children?: ReactNode;
};

function farthestCorner(x: number, y: number, width: number, height: number) {
  const dx = Math.max(x, width - x);
  const dy = Math.max(y, height - y);
  return Math.hypot(dx, dy) * 2;
}

function inkColor(color: string, opacity: number) {
  const peak = Math.min(Math.max(opacity, 0), 1);
  const pct = Math.round(peak * 100);
  return `color-mix(in srgb, ${color} ${pct}%, transparent)`;
}

function Ripple({
  className,
  duration = 650,
  color = "currentColor",
  opacity = 0.28,
  centered = false,
  children,
  style,
  onPointerDown,
  ...props
}: RippleProps) {
  const rootRef = useRef<HTMLDivElement>(null);
  const seqRef = useRef(0);
  const [marks, setMarks] = useState<RippleMark[]>([]);
  const length = Math.max(duration, 120);
  const ink = inkColor(color, opacity);

  const spawn = useCallback(
    (clientX: number, clientY: number) => {
      const el = rootRef.current;
      if (!el) return;
      if (
        typeof window !== "undefined" &&
        window.matchMedia("(prefers-reduced-motion: reduce)").matches
      ) {
        return;
      }

      const rect = el.getBoundingClientRect();
      const x = centered ? rect.width / 2 : clientX - rect.left;
      const y = centered ? rect.height / 2 : clientY - rect.top;
      const size = Math.max(farthestCorner(x, y, rect.width, rect.height), 48);
      const id = seqRef.current++;
      setMarks((current) => [...current, { id, x, y, size }]);

      window.setTimeout(() => {
        setMarks((current) => current.filter((mark) => mark.id !== id));
      }, length + 40);
    },
    [centered, length],
  );

  function handlePointerDown(event: ReactPointerEvent<HTMLDivElement>) {
    onPointerDown?.(event);
    if (event.defaultPrevented) return;
    if (event.button !== 0 && event.pointerType === "mouse") return;
    spawn(event.clientX, event.clientY);
  }

  return (
    <div
      ref={rootRef}
      data-slot="ripple"
      className={cn("arctis-ripple relative overflow-hidden", className)}
      style={
        {
          "--ripple-duration": `${length}ms`,
          ...style,
        } as CSSProperties
      }
      {...props}
      onPointerDownCapture={handlePointerDown}
    >
      {children}
      <span className="arctis-ripple-layer" aria-hidden="true">
        {marks.map((mark) => (
          <span
            key={mark.id}
            className="arctis-ripple-wave"
            style={
              {
                left: mark.x,
                top: mark.y,
                width: mark.size,
                height: mark.size,
                background: ink,
              } as CSSProperties
            }
          />
        ))}
      </span>
    </div>
  );
}

export { Ripple };
```
