canvas-confetti vs tsparticles vs party.js: Celebration Effects 2026
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 elements —
party.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} />;
}
Connecting Dots Background (Popular for Landing Pages)
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
| Feature | canvas-confetti | tsparticles | party.js |
|---|---|---|---|
| Bundle size | ~6kB | ~20-100kB | ~12kB |
| Zero dependencies | ✅ | No | No |
| React component | Hook only | ✅ <Particles> | No (imperative) |
| Confetti | ✅ Best-in-class | ✅ | ✅ |
| Fireworks | ✅ (manual) | ✅ Preset | No |
| Snow/ambient | No | ✅ | No |
| Particle network | No | ✅ | No |
| Sparkles | No | ✅ | ✅ |
| Physics engine | Basic gravity | ✅ Full | Basic |
| Interactivity | No | ✅ Hover/click | No |
| DOM anchored | Position-based | No | ✅ Element-native |
| Canvas required | ✅ | ✅ | ❌ DOM-based |
| TypeScript | ✅ | ✅ | ✅ |
| npm weekly | 2.5M | 800k | 100k |
| GitHub stars | 9k | 7k | 4.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.