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-confettiCreate 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:
- Dynamic Dimensions: Uses
window.innerWidthanddocument.documentElement.scrollHeightto cover the entire viewport and scroll area - Safari Optimization: Safari handles animation differently, so we detect it and adjust wind/gravity for smoother performance
- Responsive: Listens to window resize events to maintain proper coverage
- Z-Index Layering: Confetti sits at
zIndex: 1(background) while content sits atzIndex: 10(foreground) - 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:
truefor continuous,falsefor 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 gracefulCombining 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:
- Check z-index: Ensure confetti isn't hidden behind other elements
- Verify dimensions: Console.log your width/height to confirm they're not 0
- 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:
- react-confetti: npm package | GitHub repo
- Original gist: Background confetti
- Part 1: Button confetti explosions
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.


