Ripple Button
loomui/ripple-buttonFreeComponent
A button that sends a circle out from wherever it was pressed, sized to reach the furthest corner.
Live preview
live · rendered by the registry
Rendered by loomui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://loomui.design/r/ripple-button.jsonSource
1"use client"23import * as React from "react"45import { cn } from "@/lib/utils"67export interface RippleButtonProps extends React.ComponentProps<"button"> {8 /** Milliseconds one ripple takes to cross the button and fade out. */9 duration?: number10 /** Colour of the ripple. Keep it translucent so the label stays readable. */11 color?: string12}1314interface Ripple {15 id: number16 x: number17 y: number18 size: number19}2021/**22 * The ripple is a circle scaled up from nothing, not a growing width, so a23 * press costs one composited layer and no layout.24 */25export function RippleButton({26 children,27 className,28 duration = 280,29 color = "color-mix(in oklch, currentColor 22%, transparent)",30 onPointerDown,31 onKeyDown,32 disabled,33 style,34 ...props35}: RippleButtonProps) {36 const [ripples, setRipples] = React.useState<Ripple[]>([])37 const nextId = React.useRef(0)3839 // Measured from the press point to the furthest corner, so the circle always40 // finishes covering the button no matter where it started.41 const spawn = React.useCallback(42 (node: HTMLButtonElement, clientX?: number, clientY?: number) => {43 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {44 return45 }4647 const rect = node.getBoundingClientRect()48 const x = clientX === undefined ? rect.width / 2 : clientX - rect.left49 const y = clientY === undefined ? rect.height / 2 : clientY - rect.top50 const size =51 2 *52 Math.hypot(Math.max(x, rect.width - x), Math.max(y, rect.height - y))5354 setRipples((current) => [55 ...current,56 { id: nextId.current++, x, y, size },57 ])58 },59 []60 )6162 const drop = React.useCallback((id: number) => {63 setRipples((current) => current.filter((ripple) => ripple.id !== id))64 }, [])6566 return (67 <button68 data-slot="ripple-button"69 disabled={disabled}70 onPointerDown={(event) => {71 onPointerDown?.(event)72 if (!disabled) {73 spawn(event.currentTarget, event.clientX, event.clientY)74 }75 }}76 // Enter and Space press a button without a pointer ever touching it, so77 // the keyboard gets its own ripple from the middle rather than none.78 onKeyDown={(event) => {79 onKeyDown?.(event)80 const isPress = event.key === "Enter" || event.key === " "81 if (isPress && !event.repeat && !disabled) {82 spawn(event.currentTarget)83 }84 }}85 className={cn(86// … truncated
What it pulls in
Other registry items
Files it writes
More from loomui
All 91 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Aurora Backdropaurora-backdrop | loomui | Components | Free | no deps | |
| Bento Gridbento-grid | loomui | Components | Free | no deps | |
| Card Stackcard-stack | loomui | Components | Free | no deps | |
| Compare Slidercompare-slider | loomui | Components | Free | no deps | |
| Confetti Buttonconfetti-button | loomui | Components | Free | no deps | |
| Count Upcount-up | loomui | Components | Free | no deps | |
| Credit Cardcredit-card | loomui | Components | Free | no deps | |
| Drawerdrawer | loomui | Components | Free | 1 dep |
