Confetti Burst Button
A button that scatters a quick burst of confetti particles on click.
- Category
- Components
- Added
- 2026-08-22
- Deps
- none
- Updated
- 2026-09-01
Props
"use client";
import { useState } from "react";
type Particle = { id: number; x: number; y: number; rotate: number; color: string; size: number };
const COLORS = ["#FF2D2D", "#FAFAFA", "#E0141F", "#B8B8B8"];
let particleId = 0;
export function ConfettiButton({ label = "Click me", count = 18, spread = 70 }: {
label?: string;
count?: number;
spread?: number;
}) {
const [particles, setParticles] = useState<Particle[]>([]);
function burst() {
const next: Particle[] = Array.from({ length: count }).map(() => {
const angle = Math.random() * Math.PI * 2;
const distance = spread * (0.4 + Math.random() * 0.6);
return {
id: particleId++,
x: Math.cos(angle) * distance,
y: Math.sin(angle) * distance,
rotate: Math.random() * 360,
color: COLORS[Math.floor(Math.random() * COLORS.length)],
size: 4 + Math.random() * 5,
};
});
setParticles(next);
window.setTimeout(() => setParticles([]), 700);
}
return (
<div className="relative flex items-center justify-center">
{particles.map((p) => (
<span
key={p.id}
className="pointer-events-none absolute left-1/2 top-1/2 rounded-sm opacity-0"
style={{
width: p.size,
height: p.size,
background: p.color,
animation: "confetti-fly 700ms ease-out forwards",
"--tx": `${p.x}px`,
"--ty": `${p.y}px`,
"--tr": `${p.rotate}deg`,
} as React.CSSProperties}
/>
))}
<button onClick={burst} className="rounded-full bg-red-500 px-6 py-3 font-bold text-white">
{label}
</button>
<style jsx>{`
@keyframes confetti-fly {
0% { opacity: 1; transform: translate(-50%, -50%) translate(0, 0) rotate(0deg); }
100% { opacity: 0; transform: translate(-50%, -50%) translate(var(--tx), var(--ty)) rotate(var(--tr)); }
}
`}</style>
</div>
);
}
About this component
A confetti burst looks like it needs a canvas and a physics library, but this one is DOM spans and a single CSS keyframe animation: each particle gets a random angle and distance converted into an x/y offset via cos/sin, then animates through CSS custom properties, --tx, --ty, --tr, read inside one shared @keyframes block rather than each particle getting its own inline keyframe. The detail that actually matters for correctness: Math.random() only ever runs inside the click handler, never in the render body or a useState initializer, because generating positions eagerly at render time would bake random values into server-rendered HTML that mismatch the client's first paint, a hydration error. Reach for this over a full canvas-confetti integration when the celebration is small and localized to one button, a form submit or a mark-complete click, not a full-screen moment, where this DOM-span approach would mean hundreds of elements mounting per burst. Particles clear via a plain setTimeout matched to the animation duration; change the duration without updating that timeout and particles vanish mid-flight instead of fading.
Curator’s note
Randomness is generated only inside the click handler, never at render time — the common mistake here is computing particle positions in a useState initializer, which bakes random values into the SSR output and throws a hydration mismatch the moment the client re-renders.
Pairs well with
Matched on shared tags and category — the entries most likely to be used alongside this one.
Ripple Click Effect
componentsA Material-style ripple that expands from the exact click point.
Magnetic Button
componentsA button whose label leans toward the cursor with a springy pull.
Share Button Expand
componentsA share icon that expands into a row of social options with a copy-link state.