registry:component

Ripple Button

v1.0.0ga

Button primitive with ripple click interaction.

Registry Endpoint
Open JSON

https://registry.aavya.com/r/ui-ripple-button.json

npx shadcn@latest add https://registry.aavya.com/r/ui-ripple-button.json

Live Preview
Usage Snippet
import RippleButton from '@/components/ui/ripple-button.tsx'

export default function Example() {
  return (
    <div className="p-6">
      <RippleButton />
    </div>
  )
}
Source Preview
'use client'

import * as React from 'react'

import { motion, type HTMLMotionProps, type Transition } from 'motion/react'
import type { VariantProps } from 'class-variance-authority'

import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'

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

interface RippleButtonProps extends HTMLMotionProps<'button'>, VariantProps<typeof buttonVariants> {
  children: React.ReactNode
  scale?: number
  transition?: Transition
}

function RippleButton({
  ref,
  children,
  onClick,
  className,
  variant,
  size,
  scale = 10,
  transition = { duration: 0.6, ease: 'easeOut' },
  ...props
}: RippleButtonProps) {
  const [ripples, setRipples] = React.useState<Ripple[]>([])
  const buttonRef = React.useRef<HTMLButtonElement>(null)

  React.useImperativeHandle(ref, () => buttonRef.current as HTMLButtonElement)

  const createRipple = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
    const button = buttonRef.current

    if (!button) return

    const rect = button.getBoundingClientRect()
    const x = event.clientX - rect.left
    const y = event.clientY - rect.top

    const newRipple: Ripple = {
      id: Date.now(),
      x,
      y
    }

    setRipples(prev => [...prev, newRipple])

    setTimeout(() => {
      setRipples(prev => prev.filter(r => r.id !== newRipple.id))
    }, 600