Studio KitInteractive Buttons and Controlsmotion/react

ConfettiButton

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 props
16
70

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add confetti-button

Writes 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.

example.tsx
1import { ConfettiButton } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <ConfettiButton />6  );7}

Props

PropTypeDefault
label

Button label (ignored when children are provided).

string'Celebrate'
children

Optional custom button content.

React.ReactNodeundefined
count

Number of confetti pieces per burst.

number16
spread

Base travel distance of the burst in pixels.

number70
accentColor

Primary confetti color.

string'#38bdf8'
secondaryColor

Secondary confetti color.

string'#a78bfa'
onClick

Click handler, called on every press.

(event: React.MouseEvent<HTMLButtonElement>) => voidundefined
className

Additional class merged onto the root element.

string''

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

components/ui/confetti-button.tsx
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}