EmpireUI
Get Pro
← Blog8 min read#confetti#react#canvas

Confetti in React 2026: canvas-confetti, tsParticles and Pure CSS

Three real approaches to confetti in React — canvas-confetti, tsParticles, and zero-dependency CSS — with working code, bundle cost breakdowns, and when to use each.

colorful confetti falling against dark background celebration

Why Confetti Is Harder Than It Looks

You'd think 'drop some colored rectangles from the top of the screen' is a 20-minute job. It's not. You've got three completely different architectural choices, and they diverge fast once you factor in bundle size, React lifecycle behavior, canvas teardown, and whether your animation should respond to component unmounting or just run once and die on its own.

In 2026 the main contenders are still canvas-confetti (the lean option), tsParticles (the kitchen-sink option), and rolling your own with pure CSS keyframes (the zero-dependency option). Each one is the right call in different situations. Pick the wrong one and you're either shipping 80 kB for a three-second party effect, or spending a week debugging a CSS animation that looks wrong on Safari 17.

Honestly, most teams reach for whichever package shows up first in a Google search and then deal with the consequences. This article is the thing I wish had existed before I wasted an afternoon wrestling tsParticles config into a Next.js 14 app.

Quick aside: if you're already using Empire UI in your stack, check the components section first — there are pre-built animated backgrounds that pair well with any of these confetti techniques, and they're already tree-shaken and typed.

canvas-confetti: The Lightweight Default

canvas-confetti v1.9.3 is 17 kB minified, zero dependencies, and works by injecting a full-viewport <canvas> element and shooting particles at it with requestAnimationFrame. Install it in about 10 seconds.

npm install canvas-confetti
npm install -D @types/canvas-confetti

The React integration pattern that actually works — including proper cleanup — looks like this:

import { useCallback, useRef } from 'react';
import confetti from 'canvas-confetti';

export function ConfettiButton() {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  const fire = useCallback(() => {
    const myConfetti = confetti.create(canvasRef.current ?? undefined, {
      resize: true,
      useWorker: true, // offload to Worker thread — huge win on low-end devices
    });

    myConfetti({
      particleCount: 150,
      spread: 70,
      origin: { y: 0.6 },
      colors: ['#a78bfa', '#f472b6', '#60a5fa'],
    });
  }, []);

  return (
    <>
      <canvas ref={canvasRef} className="pointer-events-none fixed inset-0 z-50" />
      <button onClick={fire}>Celebrate</button>
    </>
  );
}

The useWorker: true flag is the thing most tutorials skip. It offloads the animation loop to a Web Worker, which keeps your main thread free for React rendering. On a Pixel 6 you'll notice the difference immediately — without it, long confetti bursts stutter during any concurrent React work. Worth noting: the canvas needs pointer-events-none or clicks on anything underneath it stop working.

That said, canvas-confetti's API surface is intentionally small. You get particle count, spread, angle, gravity, origin, colors, shapes, and scalar. That's it. The moment you want particles that orbit each other, respond to mouse position, or have physics-based collision, you've outgrown it.

tsParticles: Full Physics, Full Bundle

tsParticles via the @tsparticles/react and @tsparticles/slim packages gives you a proper 2D physics engine. We're talking velocity, acceleration, gravity, opacity lifecycle, size lifecycle, spin, wobble, mouse repulsion, click events, and yes — confetti mode. The cost is real: @tsparticles/slim alone is ~120 kB minified. The full bundle is closer to 200 kB.

npm install @tsparticles/react @tsparticles/slim
import { useCallback } from 'react';
import Particles from '@tsparticles/react';
import type { Engine } from '@tsparticles/engine';
import { loadSlim } from '@tsparticles/slim';

export function ConfettiScene() {
  const init = useCallback(async (engine: Engine) => {
    await loadSlim(engine);
  }, []);

  return (
    <Particles
      id="tsparticles-confetti"
      init={init}
      options={{
        particles: {
          number: { value: 200 },
          color: { value: ['#a78bfa', '#f472b6', '#34d399', '#fbbf24'] },
          shape: { type: ['square', 'circle'] },
          opacity: { value: { min: 0.4, max: 1 }, animation: { enable: true, speed: 0.5 } },
          size: { value: { min: 4, max: 10 } },
          rotate: { value: { min: 0, max: 360 }, animation: { enable: true, speed: 20 } },
          move: {
            enable: true,
            gravity: { enable: true, acceleration: 9.8 },
            speed: { min: 5, max: 20 },
            direction: 'bottom',
            outModes: { default: 'destroy' },
          },
        },
        emitters: {
          position: { x: 50, y: 0 },
          rate: { quantity: 10, delay: 0.1 },
          life: { count: 1, duration: 0.5 },
        },
      }}
    />
  );
}

In practice, tsParticles makes the most sense when confetti is one configuration out of several — say you're already using it for particle background effects on a hero section, and you just want to add a confetti burst on form submit. Paying the 120 kB tax once and reusing the engine everywhere is a reasonable deal. Paying it *just* for confetti is not.

One more thing — tsParticles v3 (released in 2024) moved to a plugin-based architecture, so the import paths changed. A lot of StackOverflow answers still show v2 syntax with loadFull(engine) from tsparticles. That import doesn't exist in v3. You want loadSlim from @tsparticles/slim for the lightweight build, and you need to install @tsparticles/react separately from @tsparticles/engine.

Pure CSS Confetti: Zero Dependencies, Zero Regrets

Here's the underrated option. If you need confetti that fires once on a static page — a success state, a 'you completed the tutorial' screen, a birthday card — you don't need JavaScript physics at all. A handful of CSS keyframe animations doing translateY, rotate, and opacity can convincingly fake confetti with about 60 lines of CSS and zero npm installs.

// ConfettiPure.tsx — no dependencies
import styles from './ConfettiPure.module.css';

const COLORS = ['#a78bfa', '#f472b6', '#60a5fa', '#34d399', '#fbbf24'];
const PIECES = Array.from({ length: 40 }, (_, i) => ({
  id: i,
  color: COLORS[i % COLORS.length],
  left: `${Math.random() * 100}%`,
  delay: `${Math.random() * 2}s`,
  duration: `${2 + Math.random() * 2}s`,
  size: `${6 + Math.random() * 8}px`,
}));

export function ConfettiPure() {
  return (
    <div className={styles.container} aria-hidden="true">
      {PIECES.map((p) => (
        <span
          key={p.id}
          className={styles.piece}
          style={{
            left: p.left,
            width: p.size,
            height: p.size,
            backgroundColor: p.color,
            animationDelay: p.delay,
            animationDuration: p.duration,
          }}
        />
      ))}
    </div>
  );
}
/* ConfettiPure.module.css */
.container {
  position: fixed;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 50;
}

.piece {
  position: absolute;
  top: -12px;
  border-radius: 2px;
  animation: fall linear forwards;
}

@keyframes fall {
  0%   { transform: translateY(0) rotate(0deg);   opacity: 1; }
  80%  { opacity: 1; }
  100% { transform: translateY(110vh) rotate(720deg); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .piece { animation: none; opacity: 0; }
}

Look, it won't win any physics awards. The pieces don't wobble mid-air, don't collide, and don't respond to gravity curves. But for a success modal or a micro-celebration that runs once and unmounts, this is completely sufficient. It renders in 0 ms, ships 0 extra kB, and survives any React Server Component context without a 'use client' directive fight.

The prefers-reduced-motion guard at the bottom isn't optional — users who have that set have told their OS they get headaches from this kind of thing. Respect it.

Performance Comparison and Bundle Reality Check

Let's be concrete. Here's what you're actually shipping with each option, measured with bundlephobia in August 2026:

| Approach | Minified + gzip | Main thread impact | Works in RSC? | |---|---|---|---| | Pure CSS | 0 kB | None | Yes | | canvas-confetti v1.9.3 | ~7 kB | Low (Worker option) | No (needs use client) | | @tsparticles/slim | ~42 kB | Medium | No (needs use client) | | @tsparticles/full | ~68 kB | Medium-high | No (needs use client) |

canvas-confetti with useWorker: true is the sweet spot for most single-page apps. The Worker thread means your React tree can re-render during the animation without stuttering. That matters if you're triggering confetti on form success and immediately rendering a success message — without the Worker, you'll get a visible frame drop at exactly the moment that should feel most satisfying.

One thing that catches teams out: canvas-confetti creates a real <canvas> element in the DOM, so you must call myConfetti.reset() on component unmount. Forget that and you'll leak the canvas every time the component remounts, which is easy to trigger in strict mode, hot module reloading, or React 18's concurrent features. Wrap it in a useEffect cleanup.

If performance is your obsession and you're already building dynamic backgrounds, the aurora background and canvas animations posts on this blog cover the broader canvas performance picture in React — worth reading alongside this one.

Triggering Confetti on Real Events

The pattern you'll use most is 'fire confetti when something good happens' — form submission, payment success, quiz completion. The wrong way to do it is mounting a <Confetti /> component that starts animating on mount. That means every re-render of the parent might re-trigger it, or you'll fight with key props to reset state.

The right way is an imperative API. canvas-confetti is already imperative by design. With tsParticles you can use the ParticlesContainer instance ref. Here's a reusable hook pattern that works with canvas-confetti:

import { useCallback, useEffect, useRef } from 'react';
import confetti, { type CreateTypes } from 'canvas-confetti';

export function useConfetti() {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const instanceRef = useRef<CreateTypes | null>(null);

  useEffect(() => {
    if (!canvasRef.current) return;
    instanceRef.current = confetti.create(canvasRef.current, {
      resize: true,
      useWorker: true,
    });
    return () => {
      instanceRef.current?.reset();
    };
  }, []);

  const fire = useCallback((opts?: confetti.Options) => {
    instanceRef.current?.({
      particleCount: 120,
      spread: 80,
      origin: { y: 0.65 },
      ...opts,
    });
  }, []);

  return { canvasRef, fire };
}

// Usage in any component:
// const { canvasRef, fire } = useConfetti();
// <canvas ref={canvasRef} className="pointer-events-none fixed inset-0 z-50" />
// <button onClick={() => fire()}>Submit</button>

This pattern initializes the canvas once on mount, cleans up on unmount, and gives you a stable fire function you can call anywhere. Pass different opts for different moods — a spread: 360 with startVelocity: 30 gives a fountain effect, while angle: 60 and angle: 120 from each bottom corner gives the classic two-cannon shot you've seen on Duolingo and Stripe.

Worth noting: if you're animating the rest of your UI with the same gradient generator palette as your confetti colors, the whole moment feels more designed and intentional. Cohesion in micro-interactions is what separates polished apps from ones that feel assembled.

Which One Should You Actually Use?

Simple rule: use canvas-confetti unless you have a specific reason not to. It's small, it has a Worker mode, the API is pleasant, and it stays out of your React tree. 90% of confetti use cases are covered.

Use tsParticles when: you already have it for other effects (hero particles, background animations), you need physics features canvas-confetti doesn't expose, or you're building a highly configurable experience where users can tweak the animation. Don't install 120 kB for a single burst.

Use pure CSS when: you're in a Server Component context and don't want to add 'use client' pollution, your confetti is always the same and can be statically defined, you're targeting a bundle size budget where even 7 kB matters (embedded widgets, third-party scripts), or your design team specifically wants that flat 2D paper confetti look rather than tumbling 3D pieces.

Honestly, the worst decision is spending two hours evaluating this when canvas-confetti would have been set up in 15 minutes. Ship the working version. Optimize if you see it in your Web Vitals. The kind of over-engineering that comes from too much research paralysis is exactly how 'add confetti to the checkout success page' turns into a three-day ticket.

If you want pre-built components that already handle the animation layer — including scroll-triggered reveals, animated buttons, and dynamic backgrounds — browse Empire UI for free copy-paste React components that pair well with any of these confetti approaches. The box shadow generator is also useful for the card styling around your success state.

FAQ

What's the smallest way to add confetti to a React app?

canvas-confetti at ~7 kB gzipped is the leanest JavaScript option. If you need zero kB, write 40 CSS keyframe animations instead — no npm package required.

Does canvas-confetti work with Next.js App Router?

Yes, but it needs a 'use client' directive since it accesses the DOM. Wrap it in a Client Component and import dynamically with next/dynamic if you want to keep the parent as a Server Component.

How do I stop confetti from blocking clicks on elements beneath it?

Add pointer-events-none to the canvas element. canvas-confetti injects its own canvas by default — pass your own ref and apply that class, or the canvas will swallow every click event on the page.

tsParticles vs react-confetti — which is better?

react-confetti is simpler to drop in but is declarative and harder to trigger imperatively. tsParticles is larger but far more configurable. canvas-confetti beats both for the typical 'fire once on success' use case.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Lottie Animations in React 2026: @lottiefiles/react, DotLottie SetupGSAP ScrollTrigger in React: Pinning, Scrubbing and Timeline SyncHTML Canvas Animations in React: Particles, Noise Fields, MoreCanvas Animations in React: requestAnimationFrame, Particles, Paths