Skip to main content

canvas-confetti vs tsparticles vs party.js: Celebration Effects 2026

·PkgPulse Team

canvas-confetti vs tsparticles vs party.js: Celebration Effects 2026

TL;DR

Adding confetti, fireworks, particles, and celebration effects to web apps is a quick win for delight — form submission success, milestone achievements, game completions, onboarding. Three libraries dominate this space. canvas-confetti is the obvious default — zero dependencies, 6kB gzipped, renders beautifully on a canvas element, supports confetti from any position or the cannon pattern, and is used by GitHub, Linear, and thousands of production apps. tsparticles is the Swiss army knife of particle engines — fully configurable backgrounds (snow, bubbles, connecting dots, fireworks, confetti), complex particle systems with physics, plugins for ReactJS/Vue/Svelte, but it's heavier (~20-100kB depending on plugins) and more complex to configure. party.js takes a different approach — DOM-based effects triggered off any HTML element with a high-level declarative API; it supports confetti, sparkles, and ribbons without a canvas element, making it easy to integrate with existing UI. For a quick confetti celebration on a button click: canvas-confetti. For complex ambient particle backgrounds or full particle systems: tsparticles. For element-anchored effects without canvas setup: party.js.

Key Takeaways

  • canvas-confetti is ~6kB — smallest option, zero dependencies, just works
  • tsparticles is fully configurable — 50+ presets, physics engine, plugins ecosystem
  • party.js targets DOM elementsparty.confetti(button) anchors to any element
  • canvas-confetti renders on <canvas> — fires from screen position or element
  • tsparticles has React component<Particles> component with @tsparticles/react
  • All three support React — via hooks, components, or direct import
  • canvas-confetti can fire from multiple origins — simultaneous cannons pattern

Use Case Map

Button click celebration             → canvas-confetti
Page milestone (100 orders!)         → canvas-confetti or party.js
Ambient snow/particles background    → tsparticles
Fireworks display                    → tsparticles or canvas-confetti
Element-anchored sparkle effect      → party.js
Complex interactive particle system  → tsparticles
Simple confetti burst                → canvas-confetti
Ribbon/streamer effects              → party.js or canvas-confetti

canvas-confetti: The Default Choice

canvas-confetti is the most-used confetti library — battle-tested, tiny, and visually polished.

Installation

npm install canvas-confetti
# TypeScript types:
npm install @types/canvas-confetti

Basic Confetti

import confetti from "canvas-confetti";

// Simple burst from center
confetti();

// Custom position and options
confetti({
  particleCount: 150,
  spread: 70,
  origin: { y: 0.6 },
  colors: ["#26ccff", "#a25afd", "#ff5e7e", "#88ff5a", "#fcff42"],
});

React Button with Confetti

import confetti from "canvas-confetti";
import type { Options } from "canvas-confetti";

interface ConfettiButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  children: React.ReactNode;
}

export function ConfettiButton({ children, onClick, ...props }: ConfettiButtonProps) {
  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    // Fire confetti from button position
    const rect = e.currentTarget.getBoundingClientRect();
    const x = (rect.left + rect.width / 2) / window.innerWidth;
    const y = (rect.top + rect.height / 2) / window.innerHeight;

    confetti({
      particleCount: 100,
      spread: 60,
      origin: { x, y },
      colors: ["#6366f1", "#8b5cf6", "#ec4899", "#06b6d4"],
      ticks: 200,
    });

    onClick?.(e);
  }

  return (
    <button onClick={handleClick} {...props}>
      {children}
    </button>
  );
}

Realistic Confetti from Sides

import confetti from "canvas-confetti";

function launchRealisticConfetti() {
  const duration = 5 * 1000;
  const end = Date.now() + duration;

  const colors = ["#26ccff", "#a25afd", "#ff5e7e", "#88ff5a", "#fcff42"];

  (function frame() {
    // From the left
    confetti({
      particleCount: 2,
      angle: 60,
      spread: 55,
      origin: { x: 0 },
      colors,
    });
    // From the right
    confetti({
      particleCount: 2,
      angle: 120,
      spread: 55,
      origin: { x: 1 },
      colors,
    });

    if (Date.now() < end) {
      requestAnimationFrame(frame);
    }
  })();
}

// Usage
document.getElementById("celebrate")?.addEventListener("click", launchRealisticConfetti);

School Pride / Firework Burst

import confetti from "canvas-confetti";

// School pride colors in a firework pattern
function fireConfettiBurst(x: number, y: number) {
  const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };

  const particleCount = 50;
  const colors = ["#003087", "#FFB81C"]; // Team colors

  confetti({
    ...defaults,
    particleCount,
    origin: { x, y: y - 0.1 },
    colors,
  });
  confetti({
    ...defaults,
    particleCount,
    origin: { x: x + 0.1, y },
    colors,
  });
}

// Random fireworks display
function startFireworks() {
  const duration = 3 * 1000;
  const animationEnd = Date.now() + duration;

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

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

    fireConfettiBurst(Math.random(), Math.random() - 0.2);
  }, 250);
}

useConfetti React Hook

import confetti from "canvas-confetti";
import type { Options } from "canvas-confetti";
import { useCallback } from "react";

export function useConfetti() {
  const fire = useCallback((options?: Options) => {
    confetti({
      particleCount: 100,
      spread: 70,
      origin: { y: 0.6 },
      ...options,
    });
  }, []);

  const fireFromElement = useCallback(
    (element: HTMLElement, options?: Options) => {
      const rect = element.getBoundingClientRect();
      fire({
        origin: {
          x: (rect.left + rect.width / 2) / window.innerWidth,
          y: (rect.top + rect.height / 2) / window.innerHeight,
        },
        ...options,
      });
    },
    [fire]
  );

  return { fire, fireFromElement };
}

// Usage
export function OrderSuccess() {
  const { fire } = useConfetti();

  return (
    <button onClick={() => fire({ particleCount: 200 })}>
      Place Order 🎉
    </button>
  );
}

tsparticles: Full Particle System

tsparticles is the complete particle engine — complex interactive backgrounds, physics-based particles, fireworks, snow, and more.

Installation

npm install @tsparticles/react @tsparticles/slim
# For full features:
npm install @tsparticles/all

React Confetti Component

import { useEffect, useState } from "react";
import Particles, { initParticlesEngine } from "@tsparticles/react";
import { loadSlim } from "@tsparticles/slim";
import type { ISourceOptions } from "@tsparticles/engine";

export function ConfettiBackground() {
  const [init, setInit] = useState(false);

  useEffect(() => {
    initParticlesEngine(async (engine) => {
      await loadSlim(engine);
    }).then(() => setInit(true));
  }, []);

  const options: ISourceOptions = {
    fullScreen: { enable: true, zIndex: 9999 },
    particles: {
      number: { value: 0 },
      color: {
        value: ["#26ccff", "#a25afd", "#ff5e7e", "#88ff5a", "#fcff42"],
      },
      shape: {
        type: ["circle", "square", "triangle"],
      },
      opacity: {
        value: { min: 0, max: 1 },
        animation: {
          enable: true,
          speed: 0.5,
          startValue: "max",
          destroy: "min",
        },
      },
      size: {
        value: { min: 3, max: 7 },
      },
      links: { enable: false },
      life: {
        duration: { value: 5 },
        count: 1,
      },
      move: {
        enable: true,
        gravity: { enable: true, acceleration: 15 },
        speed: { min: 5, max: 20 },
        decay: 0.1,
        direction: "none",
        random: true,
        outModes: { default: "destroy" },
      },
      rotate: {
        value: { min: 0, max: 360 },
        direction: "random",
        animation: { enable: true, speed: 30 },
      },
    },
    emitters: {
      position: { x: 50, y: 30 },
      rate: { quantity: 20, delay: 0.1 },
      life: { duration: 0.5, count: 1 },
    },
  };

  if (!init) return null;

  return <Particles id="celebration-particles" options={options} />;
}

Snow Background

import Particles, { initParticlesEngine } from "@tsparticles/react";
import { loadSlim } from "@tsparticles/slim";
import type { ISourceOptions } from "@tsparticles/engine";
import { useEffect, useState } from "react";

const snowOptions: ISourceOptions = {
  background: { color: { value: "#0d1117" } },
  particles: {
    number: { value: 80, density: { enable: true } },
    color: { value: "#ffffff" },
    shape: { type: "circle" },
    opacity: { value: { min: 0.1, max: 0.5 } },
    size: { value: { min: 1, max: 5 } },
    move: {
      direction: "bottom",
      enable: true,
      random: false,
      speed: { min: 1, max: 3 },
      straight: false,
    },
    wobble: { enable: true, distance: 5, speed: 5 },
  },
  interactivity: {
    events: {
      onHover: { enable: true, mode: "repulse" },
    },
    modes: {
      repulse: { distance: 100, duration: 0.4 },
    },
  },
};

export function SnowBackground() {
  const [init, setInit] = useState(false);

  useEffect(() => {
    initParticlesEngine(loadSlim).then(() => setInit(true));
  }, []);

  if (!init) return null;

  return <Particles options={snowOptions} />;
}
const networkOptions: ISourceOptions = {
  background: { color: { value: "#0d1117" } },
  particles: {
    number: { value: 80, density: { enable: true } },
    color: { value: "#6366f1" },
    links: {
      enable: true,
      distance: 150,
      color: "#6366f1",
      opacity: 0.3,
      width: 1,
    },
    move: {
      enable: true,
      speed: 1,
      direction: "none",
      random: true,
      straight: false,
    },
    size: { value: { min: 1, max: 3 } },
    opacity: { value: { min: 0.3, max: 0.7 } },
  },
  interactivity: {
    events: {
      onHover: { enable: true, mode: "grab" },
      onClick: { enable: true, mode: "push" },
    },
    modes: {
      grab: { distance: 200 },
      push: { quantity: 4 },
    },
  },
};

party.js: DOM-Anchored Effects

party.js fires effects from DOM elements — simpler API, no canvas setup required.

Installation

npm install party-js

Confetti from Button

import party from "party-js";
import { useRef } from "react";

export function CelebrationButton() {
  const buttonRef = useRef<HTMLButtonElement>(null);

  function handleClick() {
    if (buttonRef.current) {
      // Confetti burst from the button
      party.confetti(buttonRef.current, {
        count: party.variation.range(40, 80),
        size: party.variation.range(0.8, 1.4),
        spread: party.variation.range(35, 45),
        speed: party.variation.range(300, 600),
      });
    }
  }

  return (
    <button ref={buttonRef} onClick={handleClick}>
      🎉 Celebrate
    </button>
  );
}

Sparkle Effect

import party from "party-js";

export function SparkleButton() {
  function handleHover(e: React.MouseEvent<HTMLButtonElement>) {
    party.sparkles(e.currentTarget, {
      count: party.variation.range(10, 20),
      size: party.variation.range(0.4, 0.8),
    });
  }

  return (
    <button onMouseEnter={handleHover}>
      ✨ Hover me
    </button>
  );
}

Custom Colors and Shapes

import party from "party-js";

function fireCustomConfetti(element: HTMLElement) {
  party.confetti(element, {
    count: 60,
    // Custom shapes
    shapes: ["square", "roundedSquare"],
    // Custom color palette
    color: [
      party.Color.fromHex("#6366f1"),
      party.Color.fromHex("#8b5cf6"),
      party.Color.fromHex("#ec4899"),
    ],
  });
}

Feature Comparison

Featurecanvas-confettitsparticlesparty.js
Bundle size~6kB~20-100kB~12kB
Zero dependenciesNoNo
React componentHook only<Particles>No (imperative)
Confetti✅ Best-in-class
Fireworks✅ (manual)✅ PresetNo
Snow/ambientNoNo
Particle networkNoNo
SparklesNo
Physics engineBasic gravity✅ FullBasic
InteractivityNo✅ Hover/clickNo
DOM anchoredPosition-basedNo✅ Element-native
Canvas required❌ DOM-based
TypeScript
npm weekly2.5M800k100k
GitHub stars9k7k4.5k

When to Use Each

Choose canvas-confetti if:

  • Simple confetti burst on button click or form success
  • Want the smallest possible bundle (6kB zero deps)
  • Standard confetti pattern — cannon from bottom, burst from element
  • Already using canvas in the page
  • Need the most battle-tested, production-proven option

Choose tsparticles if:

  • Ambient background effect (snow, network, bubbles) for the whole page
  • Complex particle system with physics and interactivity
  • Fireworks or elaborate celebration sequences
  • Need a React component with declarative configuration
  • Marketing/landing page with particle effect as a design element

Choose party.js if:

  • Want effects anchored directly to DOM elements (fires from the button)
  • Simpler API — less configuration than tsparticles
  • Sparkle effect on hover for interactive UI elements
  • Don't want to manage canvas positioning manually
  • Mix of confetti + sparkle effects in the same page

Methodology

Data sourced from canvas-confetti documentation (github.com/catdad/canvas-confetti), tsparticles documentation (particles.js.org), party.js documentation (party.js.org), npm weekly download statistics as of February 2026, GitHub star counts as of February 2026, and bundle size measurements from bundlephobia.com.


Related: Framer Motion vs Motion One vs AutoAnimate for general animation libraries, or Lottie vs Rive vs CSS Animations for pre-designed animation playback.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.