Ripple Press

A soft circle grows from the exact point you press and fades out, so the button answers where it was touched.

  • CSS
  • Buttons
  • Click
  • Added

AI prompt

Create a React button with a press ripple built in CSS. On pointerdown, read the pointer position relative to the button and append a <span> at that point (position absolute, 12px circle, translate -50% -50%, background currentColor at 25% opacity). Animate it with a CSS keyframe from scale(0) opacity 1 to scale(24) opacity 0 over 600ms using cubic-bezier(0.16, 1, 0.3, 1), then remove the span on animationend. The button needs overflow: hidden and position: relative. Under prefers-reduced-motion, replace the ripple with a 120ms background colour change. The button label reads "Save changes".

Source

Demo.tsx
'use client'

import s from './demo.module.css'

export default function RipplePress({ labels }: { labels: Record<string, string> }) {
  const onDown = (e: React.PointerEvent<HTMLButtonElement>) => {
    const r = e.currentTarget.getBoundingClientRect()
    const dot = document.createElement('span')
    dot.className = s.ripple
    dot.style.left = `${e.clientX - r.left}px`
    dot.style.top = `${e.clientY - r.top}px`
    dot.addEventListener('animationend', () => dot.remove())
    e.currentTarget.appendChild(dot)
  }

  return (
    <button type="button" className={s.button} onPointerDown={onDown}>
      {labels.cta}
    </button>
  )
}
demo.module.css
.button {
  position: relative;
  overflow: hidden;
  height: 52px;
  padding: 0 32px;
  border: 0;
  border-radius: var(--radius-sm);
  background: var(--stage-accent);
  color: var(--stage-accent-ink);
  font: 500 var(--text-base) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-micro) var(--ease-out);
}

.button:focus-visible {
  outline: 2px solid var(--color-focus);
  outline-offset: 3px;
}

.ripple {
  position: absolute;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.25;
  translate: -50% -50%;
  pointer-events: none;
  animation: ripple 600ms var(--ease-out) forwards;
}

@keyframes ripple {
  from { transform: scale(0); opacity: 0.25; }
  to { transform: scale(24); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ripple { display: none; }
  .button:active { background: var(--stage-accent-hover); }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.