Skip to content
Element

Ripple Button

A micro-interaction button with a ripple spreading from the press point

  • #button
  • #micro-interaction
  • #ripple

Install

This item can't be installed yet. Use the source as a reference. Why some items can't be installed

AI prompt

Build a button that spawns a ripple span at the pointer-down position and scales it to 28x while fading out over 700ms using var(--ds-ease-out).
demo.module.csscss
.stage {
  min-height: 100vh;
  display: grid;
  place-items: center;
}

.button {
  position: relative;
  overflow: hidden;
  padding: 14px 28px;
  border: 0;
  border-radius: var(--ds-radius-md);
  background: var(--ds-color-brand-blue);
  color: var(--ds-color-on-blue);
  font-weight: var(--label-weight, 700);
  cursor: pointer;
  transition: transform 160ms var(--ds-ease-out);
}

.button:active {
  transform: scale(0.97);
}

.ripple {
  position: absolute;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.35;
  animation: ripple 700ms var(--ds-ease-out) forwards;
  pointer-events: none;
}

@keyframes ripple {
  to {
    transform: scale(28);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ripple {
    animation: none;
    opacity: 0;
  }
}
demo.tsxtsx
"use client";

import { useState, type PointerEvent } from "react";
import { playUiSound } from "@/components/arc/sound/play";
import styles from "./demo.module.css";

type Ripple = { id: number; x: number; y: number };

export default function Demo() {
  const [ripples, setRipples] = useState<Ripple[]>([]);

  const onPointerDown = (event: PointerEvent<HTMLButtonElement>) => {
    playUiSound("click");
    const rect = event.currentTarget.getBoundingClientRect();
    const id = Date.now() + Math.random();
    setRipples((current) => [...current, { id, x: event.clientX - rect.left, y: event.clientY - rect.top }]);
    window.setTimeout(() => setRipples((current) => current.filter((r) => r.id !== id)), 700);
  };

  return (
    <div className={styles.stage}>
      <button type="button" className={styles.button} onPointerDown={onPointerDown}>
        눌러 보기
        {ripples.map((ripple) => (
          <span key={ripple.id} className={styles.ripple} style={{ left: ripple.x, top: ripple.y }} />
        ))}
      </button>
    </div>
  );
}