Solid button that fires a radial burst of spinning two-tone confetti from its center on every press, with configurable piece count, spread distance, and colors.
Preview
Customize
5 propsInstallation
Copies the component source and its CSS Module into your project.
npx motionfoundry add confetti-buttonWrites components/ui/confetti-button.tsx and components/styles/confetti-button.module.css.
Usage
Updates live as you change the controls — only props that differ from the defaults are included.
1import { ConfettiButton } from "@motionfoundry/ui";2 3export function Example() {4 return (5 <ConfettiButton />6 );7}Props
Accessibility
Semantic <button type="button"> with a visible :focus-visible outline, fully keyboard operable (Enter/Space trigger onClick and the burst). Confetti layers are aria-hidden and removed after one second; pending timeouts are cleared on unmount. Under prefers-reduced-motion onClick still fires but no confetti or press scale is rendered.
Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.
Source
1"use client";2 3import * as React from "react";4import { motion, useReducedMotion } from "motion/react";5import styles from "../styles/confetti-button.module.css";6 7export interface ConfettiButtonProps {8 /** Button label (ignored when children are provided) */9 label?: string;10 /** Optional custom button content */11 children?: React.ReactNode;12 /** Number of confetti pieces per burst */13 count?: number;14 /** Base travel distance of the burst in pixels */15 spread?: number;16 /** Primary confetti color */17 accentColor?: string;18 /** Secondary confetti color */19 secondaryColor?: string;20 /** Click handler, called on every press */21 onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;22 /** Optional className */23 className?: string;24}25 26const BURST_MS = 1000;27 28export function ConfettiButton({29 label = "Celebrate",30 children,31 count = 16,32 spread = 70,33 accentColor = "#38bdf8",34 secondaryColor = "#a78bfa",35 onClick,36 className = "",37}: ConfettiButtonProps) {38 const prefersReducedMotion = useReducedMotion();39 const [bursts, setBursts] = React.useState<number[]>([]);40 const nextId = React.useRef(0);41 const timers = React.useRef(new Set<ReturnType<typeof setTimeout>>());42 43 React.useEffect(() => {44 const pending = timers.current;45 return () => {46 pending.forEach(clearTimeout);47 pending.clear();48 };49 }, []);50 51 const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {52 onClick?.(event);53 if (prefersReducedMotion) return;54 const id = nextId.current++;55 setBursts((b) => [...b, id]);56 const t = setTimeout(() => {57 timers.current.delete(t);58 setBursts((b) => b.filter((x) => x !== id));59 }, BURST_MS);60 timers.current.add(t);61 };62 63 const pieces = Math.max(1, Math.round(count));64 65 return (66 <motion.button67 type="button"68 onClick={handleClick}69 whileTap={prefersReducedMotion ? undefined : { scale: 0.92 }}70 className={`${styles.root} ${className}`.trim()}71 >72 <span className={styles.label}>{children ?? label}</span>73 {bursts.map((id) => (74 <span key={id} aria-hidden="true" className={styles.burst}>75 {Array.from({ length: pieces }, (_, i) => {76 const angle = (i / pieces) * Math.PI * 2;77 const distance = spread * (0.85 + (i % 3) * 0.3);78 return (79 <motion.span80 key={i}81 className={styles.piece}82 style={{ backgroundColor: i % 2 ? accentColor : secondaryColor }}83 initial={{ x: 0, y: 0, opacity: 1, rotate: 0, scale: 1 }}84 animate={{85 x: Math.cos(angle) * distance,86 y: Math.sin(angle) * distance,87 opacity: 0,88 rotate: 270,89 scale: 0.6,90 }}91 transition={{ duration: 0.9, ease: "easeOut" }}92 />93 );94 })}95 </span>96 ))}97 </motion.button>98 );99}