1 GitHub star = 1 new component.

Star the repo
Buttonsbuttonrippleinteractivemicro-interaction

Ripple Button

A sleek interactive button that triggers localized expanding ripple rings on click.

Live Preview

Usage

Copy and paste this code into your React component:

tsx
import { RippleButton } from 'react-component-kit-ui/buttons';

<RippleButton variant="primary" onClick={handleClick}>Click me</RippleButton>

Manual Installation

If you are not using the npm package, create a new file and paste the following code: src/components/RippleButton.tsx + RippleButton.css:

RippleButton.tsx
'use client';

import { useRef, useState } from 'react';
import type { ElementType, HTMLAttributes, PointerEvent, ReactNode } from 'react';
import './RippleButton.css';

/**
 * RippleButton โ€” Interactive button featuring an expanding click ripple effect with glass styling.
 */
export interface RippleButtonProps extends HTMLAttributes<HTMLElement> {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  href?: string;
  target?: string;
  rel?: string;
  type?: 'button' | 'submit' | 'reset';
  disabled?: boolean;
  children?: ReactNode;
}

interface Ripple {
  id: number;
  size: number;
  x: number;
  y: number;
}

export function RippleButton({
  variant = 'primary',
  size = 'md',
  href,
  type = 'button',
  disabled = false,
  className,
  children,
  onPointerDown,
  ...rest
}: RippleButtonProps) {
  const Tag: ElementType = href ? 'a' : 'button';
  const [ripples, setRipples] = useState<Ripple[]>([]);
  const nextId = useRef(0);

  const handlePointerDown = (e: PointerEvent<HTMLElement>) => {
    onPointerDown?.(e);
    if (disabled) return;
    const rect = e.currentTarget.getBoundingClientRect();
    const d = Math.max(rect.width, rect.height);
    const ripple = { id: nextId.current++, size: d, x: e.clientX - rect.left - d / 2, y: e.clientY - rect.top - d / 2 };
    setRipples((r) => [...r, ripple]);
    window.setTimeout(() => setRipples((r) => r.filter((x) => x.id !== ripple.id)), 650);
  };

  return (
    <Tag
      {...rest}
      href={href}
      type={href ? undefined : type}
      disabled={!href && disabled ? true : undefined}
      aria-disabled={disabled ? true : undefined}
      className={['ripple-btn', `ripple-btn--${variant}`, `ripple-btn--${size}`, className].filter(Boolean).join(' ')}
      onPointerDown={handlePointerDown}
    >
      <span className="ripple-btn__label">{children}</span>
      <span className="ripple-btn__container" aria-hidden="true">
        {ripples.map((r) => (
          <span key={r.id} className="ripple-effect" style={{ width: r.size, height: r.size, left: r.x, top: r.y }} />
        ))}
      </span>
    </Tag>
  );
}

export default RippleButton;
RippleButton.css
/* RippleButton โ€” ported from astro-component-kit (buttons/RippleButton.astro) */

.ripple-btn {
  --btn-primary-bg: linear-gradient(135deg, rgba(184, 134, 11, 0.9), rgba(212, 160, 23, 0.9));
  --btn-primary-color: #050505;
  --btn-secondary-bg: rgba(255, 255, 255, 0.05);
  --btn-secondary-color: var(--c-text-1, #ffffff);
  --btn-ghost-bg: transparent;
  --btn-ghost-color: var(--c-primary-light, #d4a017);

  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  font-family: var(--font-sans, system-ui, sans-serif);
  font-weight: 700;
  text-decoration: none;
  border-radius: var(--r-md, 12px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  cursor: pointer;
  user-select: none;
  transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease, border-color 0.2s ease;
}

.ripple-btn:focus-visible {
  outline: 2px solid var(--c-primary-light, #d4a017);
  outline-offset: 3px;
}

.ripple-btn[disabled],
.ripple-btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* Variants */
.ripple-btn--primary {
  background: var(--btn-primary-bg);
  color: var(--btn-primary-color);
  box-shadow: 0 4px 20px rgba(184, 134, 11, 0.25);
}
.ripple-btn--primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 30px rgba(184, 134, 11, 0.4);
  border-color: rgba(254, 243, 199, 0.4);
}

.ripple-btn--secondary {
  background: var(--btn-secondary-bg);
  color: var(--btn-secondary-color);
  backdrop-filter: blur(12px);
}
.ripple-btn--secondary:hover {
  background: rgba(255, 255, 255, 0.09);
  border-color: rgba(184, 134, 11, 0.3);
  transform: translateY(-2px);
}

.ripple-btn--ghost {
  background: var(--btn-ghost-bg);
  color: var(--btn-ghost-color);
  border-color: transparent;
}
.ripple-btn--ghost:hover {
  background: rgba(184, 134, 11, 0.1);
  border-color: rgba(184, 134, 11, 0.2);
}

/* Sizes */
.ripple-btn--sm {
  padding: 0.45rem 1rem;
  font-size: 0.8rem;
  letter-spacing: 0.02em;
}
.ripple-btn--md {
  padding: 0.75rem 1.6rem;
  font-size: 0.92rem;
  letter-spacing: 0.02em;
}
.ripple-btn--lg {
  padding: 1rem 2.2rem;
  font-size: 1.05rem;
  letter-spacing: 0.03em;
}

.ripple-btn__label {
  position: relative;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2, 0.5rem);
  pointer-events: none;
}

.ripple-btn__container {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}

.ripple-effect {
  position: absolute;
  border-radius: 50%;
  transform: scale(0);
  animation: ripple-button-ripple-grow 0.6s cubic-bezier(0, 0, 0.2, 1) forwards;
  background: rgba(255, 255, 255, 0.35);
  pointer-events: none;
}

@keyframes ripple-button-ripple-grow {
  to {
    transform: scale(4);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ripple-btn { transition: none; }
  .ripple-effect { display: none; }
}

Quick Info

Category
Buttons
Filename
RippleButton.tsx
Dependencies
None โ€” pure React + CSS
Tags
buttonrippleinteractivemicro-interaction