Bloom Logo

Confetti

A physics-based particle burst animation component built on top of canvas-confetti, supporting multiple presets and colors.

importimport { Confetti } from "@/components/ui/confetti/confetti";
$npx @bloomui-react/cli add confetti
confetti.tsx
"use client";

import confetti from "canvas-confetti";
import * as React from "react";

export interface ConfettiProps {
  fire?: boolean | number;
  variant?: "cannon" | "fireworks" | "shower" | "school-pride";
  onComplete?: () => void;
  particleCount?: number;
  angle?: number;
  spread?: number;
  startVelocity?: number;
  decay?: number;
  gravity?: number;
  drift?: number;
  ticks?: number;
  colors?: string[];
  scalar?: number;
  zIndex?: number;
  options?: confetti.Options;
}

export const Confetti: React.FC<ConfettiProps> = ({
  fire = true,
  variant = "cannon",
  onComplete,
  particleCount,
  angle,
  spread,
  startVelocity,
  decay,
  gravity,
  drift,
  ticks,
  colors,
  scalar,
  zIndex,
  options,
}) => {
  const fireAnimation = React.useCallback(() => {
    if (!fire) return;

    const baseOptions = {
      disableForReducedMotion: true,
      ...(particleCount !== undefined && { particleCount }),
      ...(angle !== undefined && { angle }),
      ...(spread !== undefined && { spread }),
      ...(startVelocity !== undefined && { startVelocity }),
      ...(decay !== undefined && { decay }),
      ...(gravity !== undefined && { gravity }),
      ...(drift !== undefined && { drift }),
      ...(ticks !== undefined && { ticks }),
      ...(colors !== undefined && { colors }),
      ...(scalar !== undefined && { scalar }),
      ...(zIndex !== undefined && { zIndex }),
      ...options,
    };

    if (variant === "cannon") {
      confetti({
        particleCount: 100,
        spread: 70,
        origin: { y: 0.6 },
        ...baseOptions,
      });
      onComplete?.();
    } else if (variant === "fireworks") {
      const duration = 3 * 1000;
      const animationEnd = Date.now() + duration;
      const defaults = {
        startVelocity: 30,
        spread: 360,
        ticks: 60,
        zIndex: 1000,
      };

      const randomInRange = (min: number, max: number) => {
        return Math.random() * (max - min) + min;
      };

      const interval: NodeJS.Timeout = setInterval(() => {
        const timeLeft = animationEnd - Date.now();

        if (timeLeft <= 0) {
          clearInterval(interval);
          onComplete?.();
          return;
        }

        const currentParticleCount =
          (particleCount || 50) * (timeLeft / duration);
        confetti({
          ...defaults,
          ...baseOptions,
          particleCount: currentParticleCount,
          origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
        });
        confetti({
          ...defaults,
          ...baseOptions,
          particleCount: currentParticleCount,
          origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
        });
      }, 250);
    } else if (variant === "shower") {
      const duration = 3 * 1000;
      const end = Date.now() + duration;

      const frame = () => {
        confetti({
          particleCount: 2,
          angle: 60,
          spread: 55,
          origin: { x: 0 },
          ...baseOptions,
        });
        confetti({
          particleCount: 2,
          angle: 120,
          spread: 55,
          origin: { x: 1 },
          ...baseOptions,
        });

        if (Date.now() < end) {
          requestAnimationFrame(frame);
        } else {
          onComplete?.();
        }
      };
      frame();
    } else if (variant === "school-pride") {
      const duration = 2 * 1000;
      const end = Date.now() + duration;

      const frame = () => {
        confetti({
          particleCount: 2,
          angle: 60,
          spread: 55,
          origin: { x: 0 },
          colors: ["#bb0000", "#ffffff"],
          ...baseOptions,
        });
        confetti({
          particleCount: 2,
          angle: 120,
          spread: 55,
          origin: { x: 1 },
          colors: ["#bb0000", "#ffffff"],
          ...baseOptions,
        });

        if (Date.now() < end) {
          requestAnimationFrame(frame);
        } else {
          onComplete?.();
        }
      };
      frame();
    }
  }, [
    fire,
    variant,
    particleCount,
    angle,
    spread,
    startVelocity,
    decay,
    gravity,
    drift,
    ticks,
    colors,
    scalar,
    zIndex,
    options,
    onComplete,
  ]);

  React.useEffect(() => {
    fireAnimation();
  }, [fireAnimation]);

  return null;
};

Confetti.displayName = "Confetti";

Default

Trigger a single burst from the center of the screen.

fire: boolean | numbervariant: cannon | fireworks | shower | school-pride

Presets

Try out the different preset physics patterns: fireworks, falling shower, or themed school pride colors.

variant: cannon | fireworks | shower | school-pride

Custom Colors

Pass a list of hex codes to the colors prop to change the palette of the particles.

colors: string[]

Density & Spread

Configure the particleCount and spread to control the size and direction of the explosion.

particleCount: numberspread: number

Wind & Gravity

Fiddle with gravity (weight) and drift (wind speed) to float particles sideways.

gravity: numberdrift: numberstartVelocity: number

Particle Scale

Use the scalar prop to adjust the size of the individual confetti particles.

scalar: number
API Reference

Props — Confetti

Detailed specification of the Confetti component properties.

PropTypeDefaultDescription
fireboolean | numbertrueTriggers a new animation burst when the value changes or is set to true.
variant"cannon" | "fireworks" | "shower" | "school-pride""cannon"The layout movement preset of the confetti particles.
particleCountnumber100 / 50Total number of confetti particles per burst.
spreadnumber70Angle range spread of the burst particles in degrees.
colorsstring[]undefinedCustom color hex string array for particles.
anglenumber90Angle direction from which particles rise (e.g. 90 is straight up).
startVelocitynumber45Initial speed of launched particles (higher values shoot faster/higher).
decaynumber0.9Friction factor determining how fast particles slow down.
gravitynumber1Gravity factor pulling particles downwards. Decreasing floats particles longer.
driftnumber0Sideways drift force simulating wind direction effects.
ticksnumber200Lifetime limit of animation frames before particles disappear.
scalarnumber1Scaling size multiplier of the confetti particles.
zIndexnumber100CSS z-index of the confetti rendering canvas layer.
optionsconfetti.OptionsundefinedStandard options passed directly to the canvas-confetti library.
onComplete() => voidundefinedCallback fired when a particle burst completes.