Add Continuous Background Confetti to Next.js & React

10 min read
November 24, 2025
Add Continuous Background Confetti to Next.js & React

Welcome to Part 2 of our confetti animation series! In Part 1, we covered button-triggered confetti explosions—perfect for celebrating user actions. Now, let's take it to the next level with continuous background confetti.

This implementation creates a constant, gentle confetti rain in the background of your entire app—perfect for landing pages, event sites, or adding brand personality. It's exactly what we use on ConfettiSaaS.com to create a memorable first impression.

By the end of this guide, you'll have smooth, performant background confetti that works across all browsers and devices.

Prerequisites

Before we begin, make sure you have:

  • A Next.js 13+ or React 18+ project set up
  • Basic understanding of React hooks (useState, useEffect)
  • Node.js and npm/yarn installed

Already completed Part 1? Great! This builds on those concepts but uses a different library.

Continuous Background Confetti

This creates a constant, gentle confetti rain in the background of your entire app—perfect for landing pages, event sites, or adding brand personality.

Install the Package

Install react-confetti (different from the button package in Part 1):

npm install react-confetti
# or
yarn add react-confetti

Create the ConfettiComponent

Create components/ConfettiComponent.tsx. Let's build this step by step:

Step 1: Set up imports and state

"use client";
 
import Confetti from "react-confetti";
import { useState, useEffect } from "react";
 
export default function ConfettiComponent() {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
  const [isSafari, setIsSafari] = useState(false);

We initialize dimensions to 0 to avoid hydration errors in Next.js (server and client render the same initially).

Step 2: Handle responsive dimensions

  useEffect(() => {
    const updateDimensions = () => {
      setDimensions({
        width: window.innerWidth,
        height: document.documentElement.scrollHeight,
      });
    };
 
    if (typeof window !== "undefined") {
      updateDimensions();
      window.addEventListener("resize", updateDimensions);

This ensures confetti covers the entire viewport and updates when the window resizes.

Step 3: Detect Safari for optimization

      // Detect if the browser is Safari
      const userAgent = window.navigator.userAgent;
      const isSafariBrowser = /^((?!chrome|android).)*safari/i.test(userAgent);
      setIsSafari(isSafariBrowser);
    }
 
    // Cleanup event listener on component unmount
    return () => {
      if (typeof window !== "undefined") {
        window.removeEventListener("resize", updateDimensions);
      }
    };
  }, []);

Safari handles animations differently, so we detect it to adjust physics settings for smoother performance.

Step 4: Render the confetti

  return (
    <div style={{ position: "fixed", zIndex: 1 }}>
      <Confetti
        width={dimensions.width}
        height={dimensions.height}
        wind={isSafari ? 0.1 : 0.02}
        gravity={isSafari ? 0.1 : 0.04}
        numberOfPieces={30}
        recycle={true}
      />
    </div>
  );
}

The confetti sits at zIndex: 1 so it stays in the background. Setting recycle={true} makes it loop infinitely.

Integrate into Your Layout

For Next.js App Router, modify your app/layout.tsx to add confetti site-wide.

Import the component:

import ConfettiComponent from "@/components/ConfettiComponent";

Add proper z-index layering:

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <div>
          {/* Confetti in background (z-index: 1) */}
          <ConfettiComponent />
          
          {/* Content in foreground (z-index: 10) */}
          <div style={{ position: "relative", zIndex: 10 }}>
            <div className="min-h-screen">
              {children}
            </div>
          </div>
        </div>
      </body>
    </html>
  );
}

The key is the z-index layering: confetti at 1, content at 10. This keeps confetti visible but behind all your content.

How This Works

Key Implementation Details:

  1. Dynamic Dimensions: Uses window.innerWidth and document.documentElement.scrollHeight to cover the entire viewport and scroll area
  2. Safari Optimization: Safari handles animation differently, so we detect it and adjust wind/gravity for smoother performance
  3. Responsive: Listens to window resize events to maintain proper coverage
  4. Z-Index Layering: Confetti sits at zIndex: 1 (background) while content sits at zIndex: 10 (foreground)
  5. Recycle Mode: recycle={true} makes confetti pieces loop infinitely

Configuration Options

Customize the confetti effect by adjusting these props:

  • numberOfPieces: More pieces = denser confetti (we use 30 for subtle effect)
  • wind: Horizontal drift (-1 to 1, positive = right)
  • gravity: Fall speed (higher = faster, 0.1 is gentle)
  • recycle: true for continuous, false for one-time burst
  • colors: Array of colors (defaults to rainbow)

Performance Considerations

Background confetti runs continuously, so optimize for performance. Keep numberOfPieces under 50 for smooth mobile experience.

Performance Tips:

// Good for mobile
numberOfPieces={20}
gravity={0.08} // Faster fall = less rendering time
 
// Good for desktop
numberOfPieces={40}
gravity={0.04} // Slower, more graceful

Combining Both Approaches

Want both background confetti AND button explosions from Part 1? Absolutely! They use different libraries and won't conflict.

When to Use Each:

Background Confetti:

  • Landing pages
  • Event/seasonal promotions
  • Brand-heavy pages
  • Celebration pages (after signup, purchase)

Button-Triggered Confetti:

  • Call-to-action buttons
  • Form submissions
  • Achievement unlocks
  • Purchase confirmations

Using Both Together:

// layout.tsx - for background
<ConfettiComponent />
 
// pricing page - for button (from Part 1)
<BuyButton url="/checkout" />

The z-index layering ensures they don't interfere—background confetti stays behind content, button confetti appears on top.

Customization Tips

Match Your Brand Colors

Replace default colors with your brand palette:

// Background confetti
<Confetti
  colors={["#your-primary", "#your-secondary", "#your-accent"]}
  numberOfPieces={30}
/>

Seasonal Themes

Halloween:

colors={["#ff6600", "#000000", "#9b59b6"]}

Christmas:

colors={["#ff0000", "#00ff00", "#ffffff", "#ffd700"]}

Valentine's Day:

colors={["#ff69b4", "#ff1493", "#ff69b4", "#ffffff"]}

Conditional Display

Show confetti only on specific pages:

'use client'
 
import { usePathname } from 'next/navigation';
import Confetti from 'react-confetti';
 
export default function ConditionalConfetti() {
  const pathname = usePathname();
  const showConfetti = pathname === '/' || pathname.startsWith('/pricing');
  
  if (!showConfetti) return null;
  
  return <Confetti {...props} />;
}

Common Pitfalls & Solutions

Hydration Errors in Next.js

Problem: "Hydration failed because the initial UI does not match what was rendered on the server."

Solution: Always use 'use client' directive and initialize dimensions to 0:

const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

This prevents server-rendered content from differing from client-rendered content.

Performance Issues on Mobile

Problem: Background confetti causes lag on mobile devices.

Solution: Reduce particle count or disable on mobile:

const isMobile = window.innerWidth < 768;
const pieces = isMobile ? 15 : 30;
 
<Confetti numberOfPieces={pieces} />

Or detect device type:

useEffect(() => {
  const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
  setNumberOfPieces(isMobile ? 15 : 30);
}, []);

Confetti Not Appearing

Problem: Confetti renders but isn't visible.

Solutions:

  1. Check z-index: Ensure confetti isn't hidden behind other elements
  2. Verify dimensions: Console.log your width/height to confirm they're not 0
  3. Check for CSS conflicts: Some CSS resets might affect the confetti container
// Debug dimensions
useEffect(() => {
  console.log('Confetti dimensions:', dimensions);
}, [dimensions]);

Confetti Not Covering Full Page

Problem: Confetti only appears in viewport, not over scrolled content.

Solution: Use document.documentElement.scrollHeight instead of window.innerHeight:

setDimensions({
  width: window.innerWidth,
  height: document.documentElement.scrollHeight, // Full page height
});

Safari Performance Issues

Problem: Animation stutters or lags in Safari.

Solution: Adjust wind and gravity specifically for Safari:

const isSafariBrowser = /^((?!chrome|android).)*safari/i.test(userAgent);
 
<Confetti
  wind={isSafariBrowser ? 0.1 : 0.02}
  gravity={isSafariBrowser ? 0.1 : 0.04}
/>

Higher values make confetti fall faster, reducing rendering time.

Real-World Examples

Success Page After Signup

Here's a practical example: one-time confetti burst that auto-stops after 5 seconds.

Create a success page with timed confetti:

'use client'
 
import { useEffect, useState } from 'react';
import Confetti from 'react-confetti';
 
export default function SuccessPage() {
  const [showConfetti, setShowConfetti] = useState(true);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
 
  useEffect(() => {
    // Set dimensions
    setDimensions({
      width: window.innerWidth,
      height: window.innerHeight
    });
    
    // Stop confetti after 5 seconds
    const timer = setTimeout(() => setShowConfetti(false), 5000);
    return () => clearTimeout(timer);
  }, []);
 
  return (
    <div>
      {showConfetti && (
        <Confetti
          width={dimensions.width}
          height={dimensions.height}
          recycle={false}
        />
      )}
      <h1>Welcome aboard! 🎉</h1>
      <p>Your account has been created successfully.</p>
    </div>
  );
}

Key difference: recycle={false} makes confetti fall once and stop, perfect for one-time celebrations.

Landing Page with Branded Confetti

Create a memorable first impression with branded colors:

'use client'
 
import ConfettiComponent from '@/components/ConfettiComponent';
 
export default function LandingPage() {
  return (
    <div className="relative">
      {/* Branded background confetti */}
      <div style={{ position: "fixed", zIndex: 1 }}>
        <Confetti
          width={window.innerWidth}
          height={document.documentElement.scrollHeight}
          numberOfPieces={25}
          colors={["#667eea", "#764ba2", "#f093fb", "#4facfe"]}
          recycle={true}
          gravity={0.05}
        />
      </div>
      
      {/* Page content */}
      <div style={{ position: "relative", zIndex: 10 }}>
        <h1>Your Awesome SaaS Product</h1>
        <p>Making the web more fun, one confetti at a time</p>
      </div>
    </div>
  );
}

Event Countdown with Confetti

Trigger confetti when countdown reaches zero:

'use client'
 
import { useState, useEffect } from 'react';
import Confetti from 'react-confetti';
 
export default function CountdownPage() {
  const [showConfetti, setShowConfetti] = useState(false);
  const [timeLeft, setTimeLeft] = useState(10);
 
  useEffect(() => {
    if (timeLeft > 0) {
      const timer = setTimeout(() => setTimeLeft(timeLeft - 1), 1000);
      return () => clearTimeout(timer);
    } else {
      setShowConfetti(true);
    }
  }, [timeLeft]);
 
  return (
    <div>
      {showConfetti && (
        <Confetti
          width={window.innerWidth}
          height={window.innerHeight}
          numberOfPieces={200}
          recycle={false}
        />
      )}
      <h1>{timeLeft > 0 ? `${timeLeft}...` : '🎉 Happy New Year!'}</h1>
    </div>
  );
}

Advanced Optimization Techniques

Lazy Loading Confetti

Only load the confetti library when needed:

'use client'
 
import { useState, useEffect, lazy, Suspense } from 'react';
 
const Confetti = lazy(() => import('react-confetti'));
 
export default function OptimizedConfetti() {
  const [showConfetti, setShowConfetti] = useState(false);
 
  useEffect(() => {
    // Only show confetti on homepage
    if (window.location.pathname === '/') {
      setShowConfetti(true);
    }
  }, []);
 
  if (!showConfetti) return null;
 
  return (
    <Suspense fallback={null}>
      <Confetti {...props} />
    </Suspense>
  );
}

Reduced Motion Support

Respect user preferences for reduced motion:

useEffect(() => {
  const prefersReducedMotion = window.matchMedia(
    '(prefers-reduced-motion: reduce)'
  ).matches;
  
  if (prefersReducedMotion) {
    setNumberOfPieces(0); // Disable confetti
  }
}, []);

Dynamic Particle Count Based on Performance

Adjust confetti density based on device capabilities:

useEffect(() => {
  // Check if device is low-end
  const connection = (navigator as any).connection;
  const isSlowConnection = connection?.effectiveType === '2g' || 
                          connection?.effectiveType === 'slow-2g';
  const isLowEndDevice = navigator.hardwareConcurrency && 
                         navigator.hardwareConcurrency < 4;
  
  if (isSlowConnection || isLowEndDevice) {
    setNumberOfPieces(10);
  } else {
    setNumberOfPieces(30);
  }
}, []);

Package Documentation & Resources

Want to dive deeper? Check out the official documentation:

Wrapping Up

You now have production-ready continuous background confetti that's:

Performant: Optimized for mobile and Safari
Responsive: Adapts to window resizing
Customizable: Match your brand colors and intensity
Accessible: Supports reduced motion preferences

Combined with the button confetti from Part 1, you have two powerful ways to add personality and delight to your React or Next.js app.

Quick Recap: When to Use Each

  • Button Confetti (Part 1): Celebrate specific user actions (purchases, submissions, achievements)
  • Background Confetti (Part 2): Create brand atmosphere on landing pages, events, or special promotions

Start with background confetti on your homepage to create an immediate memorable impression. Add button confetti to key conversion points to celebrate user success.

Ready to make your app more engaging? Drop this implementation into your project and watch how it transforms your user experience. And if you're building a SaaS product, list it on ConfettiSaaS for visibility and backlinks!

Have questions about implementing background confetti? Found creative ways to optimize performance? Let's chat on Twitter or in the comments below.

👨‍💻
Written by
Lars
Building ConfettiSaaS and helping SaaS founders grow.
Focus: SaaS growth, marketing, and SEO. Also, I love confetti.