EmpireUI
Get Pro
← Blog7 min read#confetti-animation#react#css-animation

Confetti Animation in React: Celebration UI for Milestones

Add confetti animations to React apps for milestone moments — signups, purchases, completions. Real code, zero fluff, with canvas and CSS approaches compared.

Colorful confetti pieces falling against a dark background during a celebration event

Why Confetti Actually Matters in UI

Honestly, confetti is one of the most underused UI patterns in production apps. Developers ship entire onboarding flows, checkout funnels, and completion screens — then drop the user into a blank page with zero fanfare. That's a missed opportunity.

The moment a user completes something meaningful — a first purchase, a streak milestone, a project launch — their brain is primed for reward. A burst of confetti lasts maybe 2.5 seconds. But it signals to the user that the app noticed. That it cares. And that moment sticks in memory far longer than any email confirmation.

This article is about implementing confetti in React the right way: performant, accessible, and actually fun to build. We'll cover canvas-based approaches, CSS-only options, and when to reach for a library versus rolling your own.

Canvas vs CSS: Choosing Your Confetti Approach

There are two main ways to render confetti in a browser: the HTML5 Canvas API and pure CSS animations. Each has a real tradeoff you should understand before writing a line of code.

Canvas wins on particle count. If you want 300+ confetti pieces falling simultaneously with physics — gravity, drag, rotation — canvas is the only sane choice. The GPU handles the rendering pipeline and your DOM stays completely clean. Libraries like canvas-confetti (v1.9.3 as of late 2026) use this approach and can fire 500 particles in under 16ms.

CSS animations shine when you want a smaller, stylized burst — maybe 20-40 pieces — with full control over shape, color, and timing using your existing Tailwind v4 classes. The DOM cost is real, but for short-lived celebration moments it's usually fine. If you're already using aurora background effects or other ambient animations on the same page, lean toward canvas to avoid compounding paint work.

Installing and Using canvas-confetti in React

The fastest path to confetti is canvas-confetti. One import, one function call, done. Here's a minimal but realistic React component that fires confetti when a button is clicked — the exact pattern you'd use on a "Payment Complete" screen.

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

export function CelebrationButton({ label }: { label: string }) {
  const buttonRef = useRef<HTMLButtonElement>(null);

  const handleCelebrate = useCallback(() => {
    const rect = buttonRef.current?.getBoundingClientRect();
    const originX = rect ? (rect.left + rect.width / 2) / window.innerWidth : 0.5;
    const originY = rect ? (rect.top + rect.height / 2) / window.innerHeight : 0.5;

    confetti({
      particleCount: 160,
      spread: 80,
      startVelocity: 45,
      origin: { x: originX, y: originY },
      colors: ['#6366f1', '#a855f7', '#ec4899', '#f59e0b', '#10b981'],
      ticks: 300,
      gravity: 1.2,
      scalar: 1.1,
    });
  }, []);

  return (
    <button
      ref={buttonRef}
      onClick={handleCelebrate}
      className="px-6 py-3 bg-indigo-600 text-white rounded-xl font-semibold hover:bg-indigo-700 transition-colors"
    >
      {label}
    </button>
  );
}

A few things worth noting here. The origin is computed from the button's actual position so the burst feels like it's coming from the element itself — not from the top of the screen. The ticks: 300 controls how long particles live. gravity: 1.2 makes the fall feel slightly weighted. Tweak scalar: 1.1 to make particles 10% larger than default.

Building a CSS-Only Confetti Burst with Tailwind

Sometimes you don't want an external dependency. Sometimes you want confetti that's perfectly on-brand — specific shapes, specific colors from your design token system. CSS animations can handle this if you keep the particle count under ~40.

The pattern is: generate N absolutely-positioned <span> elements, each with a random initial X position, randomized animation-delay, and a keyframe that moves it from origin downward while rotating. In Tailwind v4, you'll write the keyframe in your global CSS and apply it via arbitrary value classes.

/* globals.css */
@keyframes confetti-fall {
  0% {
    transform: translateY(-10px) rotate(0deg);
    opacity: 1;
  }
  100% {
    transform: translateY(100vh) rotate(720deg);
    opacity: 0;
  }
}

.confetti-piece {
  position: absolute;
  width: 8px;
  height: 8px;
  border-radius: 2px;
  animation: confetti-fall linear forwards;
  will-change: transform, opacity;
  background-color: rgba(255, 255, 255, 0.15); /* tinted base */
}

Then in your React component, generate the pieces with Array.from({ length: 36 }) and randomize left, animation-duration (between 1.8s and 3.2s), animation-delay (0 to 0.9s), and background color from your palette. Wrap them in a fixed-position container with pointer-events: none so they don't block user interaction. Clean them up after the longest animation completes using a setTimeout.

Triggering Confetti on Real App Events

A confetti burst sitting behind a demo button is useless. You want it wired to actual app state. The three most common triggers in production apps: form submission success, streak/achievement unlock, and e-commerce order confirmation.

For async flows — where you're waiting on a server response — the cleanest pattern is a custom hook. The hook listens to a status value (idle, loading, success, error) and fires confetti exactly once when status transitions to 'success'. Using a ref to track whether it's already fired prevents double-triggers in React Strict Mode.

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

type Status = 'idle' | 'loading' | 'success' | 'error';

export function useConfettiOnSuccess(status: Status) {
  const firedRef = useRef(false);

  useEffect(() => {
    if (status === 'success' && !firedRef.current) {
      firedRef.current = true;
      confetti({
        particleCount: 200,
        spread: 100,
        origin: { y: 0.55 },
        colors: ['#6366f1', '#a855f7', '#f59e0b'],
      });
    }
    if (status !== 'success') {
      firedRef.current = false;
    }
  }, [status]);
}

Drop this hook into any component that receives a status prop. It resets when status leaves 'success', so if the user retries and succeeds again, they get another burst. That's intentional — repetition of positive feedback is fine here.

Accessibility and Motion Sensitivity

Here's the thing: not everyone wants to see rapid movement on screen. The prefers-reduced-motion media query exists for a reason — vestibular disorders, epilepsy, and motion sensitivity affect a non-trivial percentage of your users. Ignoring this for a celebration effect is a bad call.

The fix is simple. Check window.matchMedia('(prefers-reduced-motion: reduce)').matches before firing confetti. If it's true, show a static success state instead — a green checkmark, a brief toast, something that communicates the same positive signal without motion. This is about 4 lines of code and it matters.

Also consider: confetti canvas layers should carry aria-hidden="true" if you render them as DOM elements. Screen readers don't need to announce decorative animations. Keep your meaningful success message — "Order placed!" or whatever — in regular markup so it's picked up correctly. If you're building out a full design system with multiple animation layers, check out how particles background components handle this same concern — the pattern translates directly.

Performance Optimization for Confetti at Scale

Canvas-based confetti is fast, but not free. On low-end Android devices, firing 500 particles with requestAnimationFrame can still cause jank if your page already has expensive paint work happening — like a chart re-render or a large image lazy-loading in the same frame.

Use requestIdleCallback to defer the confetti call by a frame or two if other critical UI is updating simultaneously. Or fire it on a 60ms setTimeout after the success state is set — by then, the state update has painted and the animation loop has breathing room. You can also reduce particleCount on mobile using navigator.hardwareConcurrency as a rough signal: if it's 4 or fewer cores, drop from 200 to 80 particles.

For multi-burst effects — think progressive celebration like a 10-step wizard completion — use the confetti.create() API to get an instance tied to a specific canvas element. This lets you control z-index precisely and dispose of the canvas when you're done. Way cleaner than stacking canvas elements in the global DOM. If you're exploring other ambient background effects for your UI, the shooting stars background uses a similar canvas lifecycle management pattern worth studying.

Combining Confetti with Sound and Haptics

Visual confetti is great. Confetti plus a short audio cue? That's a full sensory moment. The Web Audio API lets you play a short celebratory tone without loading a sound file — programmatically generated using an oscillator, which is a 10-line implementation. Keep it under 300ms and under 40% volume or it'll feel jarring instead of joyful.

On mobile, the Vibration API (navigator.vibrate([100, 50, 100])) adds haptic feedback on Android Chrome. It's a double-tap pattern — short, pause, short — that feels like a thumbs up rather than an alert. iOS Safari doesn't support the Vibration API (as of late 2026), so always check for support before calling it. Feature-detect, don't assume.

The combination of confetti + sound + haptics creates a multi-modal moment that users actually remember. Used sparingly — only for genuinely significant milestones — it can become a signature part of your product's personality. Think about how your theme toggle or other micro-interactions reinforce your app's character. Celebration UI is no different.

FAQ

What's the lightest way to add confetti to a React app without a full library?

The canvas-confetti package is only ~6KB gzipped and has zero dependencies. It's the most practical choice for most projects. If you truly want zero external dependencies, a CSS-only approach with 20-30 absolutely positioned divs and a keyframe animation works — but you'll lose physics and it's more code to maintain.

How do I prevent confetti from firing twice in React Strict Mode?

Use a useRef flag as a guard inside your useEffect. Set firedRef.current = true immediately when you trigger confetti, then check !firedRef.current before firing. Strict Mode double-invokes effects in development, so without the guard you'll get two bursts. The ref persists across re-renders without causing re-renders itself.

Can I customize confetti shapes — like stars or hearts — with canvas-confetti?

Yes. The shapes option accepts 'square', 'circle', and since v1.9.0 you can pass custom shapes using confetti.shapeFromPath() with an SVG path string. For a heart, you'd pass the SVG heart path data and add it to the shapes array. The library handles the canvas drawing for you.

How do I make confetti respect prefers-reduced-motion?

Check window.matchMedia('(prefers-reduced-motion: reduce)').matches before calling the confetti function. If it returns true, skip the animation entirely and just render a static success indicator. You can also listen for changes to this preference with addEventListener('change', ...) on the MediaQueryList if your app needs to respond dynamically.

My confetti appears behind my modal overlay. How do I fix z-index issues?

The canvas-confetti library creates a canvas element that it appends to the document body. By default it has a high z-index, but if your modal is using something like z-index: 9999, the canvas may fall behind it. Use confetti.create(canvas, { resize: true }) with your own canvas element that you position and z-index correctly, then pass that instance to your fire function.

How do I fire confetti from a specific element rather than from the center of the screen?

Use getBoundingClientRect() on the element to get its position, then compute origin.x as (rect.left + rect.width / 2) / window.innerWidth and origin.y as (rect.top + rect.height / 2) / window.innerHeight. Pass those normalized values (0 to 1) to the origin option. This makes the burst appear to come from the button or card itself.

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

Read next

CSS Animation Performance: GPU Layers, will-change, 60fpsCSS Keyframe Animation Library: 20 Copy-Paste EffectsTailwind Animation Library: 30 Classes for Common EffectsQR Code Generator in React: Custom Logo, Download, Share