Skip to content

Confetti Burst Button

A button that scatters a quick burst of confetti particles on click.

Freeplayfulneon
Category
Components
Added
2026-08-22
Deps
none
Updated
2026-09-01
Preview

Props

18
70
No runtime dependencies
"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>
  );
}
Notes

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.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

A Material-style ripple that expands from the exact click point.

ripplebuttonclick
Featured

Magnetic Button

components

A button whose label leans toward the cursor with a springy pull.

buttoncursorspring

A share icon that expands into a row of social options with a copy-link state.

sharebuttonexpand