Confetti Button
loomui/confetti-buttonFreeComponent
A button that throws a handful of paper into the air on press, each piece lobbed on its own arc.
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/confetti-button.jsonSource
1"use client"23import * as React from "react"45import { cn } from "@/lib/utils"67export interface ConfettiButtonProps extends React.ComponentProps<"button"> {8 /** Pieces thrown by one press. */9 count?: number10 /** Milliseconds a piece stays in the air. */11 duration?: number12 /** Furthest a piece travels sideways, in pixels. */13 spread?: number14 /** Colours a piece is picked from. */15 colors?: string[]16}1718interface Piece {19 id: number20 drift: number21 rise: number22 fall: number23 spin: number24 size: number25 color: string26 radius: string27}2829const DEFAULT_COLORS = ["#2dd4bf", "#38bdf8", "#3b82f6", "#67e8f9", "#fbbf24"]3031/**32 * Two nested spans per piece, because a lob is two motions on two clocks: the33 * outer drifts sideways at a constant rate while the inner rises and falls.34 * One element could only ever travel in a straight line.35 */36export function ConfettiButton({37 children,38 className,39 count = 18,40 duration = 900,41 spread = 90,42 colors = DEFAULT_COLORS,43 onClick,44 disabled,45 style,46 ...props47}: ConfettiButtonProps) {48 const [pieces, setPieces] = React.useState<Piece[]>([])49 const nextId = React.useRef(0)50 const timers = React.useRef<number[]>([])5152 React.useEffect(() => {53 const pending = timers.current54 return () => {55 for (const timer of pending) {56 window.clearTimeout(timer)57 }58 }59 }, [])6061 const burst = React.useCallback(() => {62 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {63 return64 }6566 const palette = colors.length > 0 ? colors : DEFAULT_COLORS67 const thrown: Piece[] = Array.from({ length: count }, () => {68 // A fan across the top rather than a full circle. Pieces that start69 // downward read as a leak, not a throw.70 const angle = (-160 + Math.random() * 140) * (Math.PI / 180)71 const reach = spread * (0.45 + Math.random() * 0.55)7273 return {74 id: nextId.current++,75 drift: Math.cos(angle) * reach,76 rise: Math.sin(angle) * reach,77 fall: Math.abs(Math.sin(angle) * reach) + spread * 0.85,78 spin: Math.round(Math.random() * 720 - 360),79 size: 5 + Math.round(Math.random() * 4),80 color: palette[Math.floor(Math.random() * palette.length)],81 // A mix of chips and dots, so a burst does not read as one shape82 // repeated at different angles.83 radius: Math.random() < 0.4 ? "9999px" : "1px",84 }85 })8687 setPieces((current) => [...current, ...thrown])8889// … 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 | |
| Count Upcount-up | loomui | Components | Free | no deps | |
| Credit Cardcredit-card | loomui | Components | Free | no deps | |
| Drawerdrawer | loomui | Components | Free | 1 dep | |
| Elastic Tabselastic-tabs | loomui | Components | Free | no deps |
