React Fireworks Background
React fireworks background with click-triggered explosions. Canvas particle physics creates Fourth of July effects with TypeScript and shadcn/ui.
Trying to implement particle explosions?
Join our Discord community for help from other developers.
Who doesn't love fireworks? This React component brings that explosive celebration to your web app. Click anywhere and watch particles arc through the air, explode into brilliant bursts, then fade away with realistic physics. It's like having a fireworks show in your browser, minus the noise complaints.
Interactive celebration effects
Click anywhere to launch fireworks with realistic physics and gravity:
'use client'; import { FireworksBackground } from '@/components/ui/shadcn-io/fireworks-background';import { useTheme } from 'next-themes';const FireworksBackgroundDemo = () => { const { resolvedTheme: theme } = useTheme(); return ( <FireworksBackground className="absolute inset-0 flex items-center justify-center rounded-xl" color={theme === 'dark' ? 'white' : 'black'} /> );};export default FireworksBackgroundDemo;
'use client'; import * as React from 'react'; import { cn } from '@/lib/utils'; const rand = (min: number, max: number): number => Math.random() * (max - min) + min; const randInt = (min: number, max: number): number => Math.floor(Math.random() * (max - min) + min); const randColor = (): string => `hsl(${randInt(0, 360)}, 100%, 50%)`; type ParticleType = { x: number; y: number; color: string; speed: number; direction: number; vx: number; vy: number; gravity: number; friction: number; alpha: number; decay: number; size: number; update: () => void; draw: (ctx: CanvasRenderingContext2D) => void; isAlive: () => boolean;}; function createParticle( x: number, y: number, color: string, speed: number, direction: number, gravity: number, friction: number, size: number,): ParticleType { const vx = Math.cos(direction) * speed; const vy = Math.sin(direction) * speed; const alpha = 1; const decay = rand(0.005, 0.02); return { x, y, color, speed, direction, vx, vy, gravity, friction, alpha, decay, size, update() { this.vx *= this.friction; this.vy *= this.friction; this.vy += this.gravity; this.x += this.vx; this.y += this.vy; this.alpha -= this.decay; }, draw(ctx: CanvasRenderingContext2D) { ctx.save(); ctx.globalAlpha = this.alpha; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fillStyle = this.color; ctx.fill(); ctx.restore(); }, isAlive() { return this.alpha > 0; }, };} type FireworkType = { x: number; y: number; targetY: number; color: string; speed: number; size: number; angle: number; vx: number; vy: number; trail: { x: number; y: number }[]; trailLength: number; exploded: boolean; update: () => boolean; explode: () => void; draw: (ctx: CanvasRenderingContext2D) => void;}; function createFirework( x: number, y: number, targetY: number, color: string, speed: number, size: number, particleSpeed: { min: number; max: number } | number, particleSize: { min: number; max: number } | number, onExplode: (particles: ParticleType[]) => void,): FireworkType { const angle = -Math.PI / 2 + rand(-0.3, 0.3); const vx = Math.cos(angle) * speed; const vy = Math.sin(angle) * speed; const trail: { x: number; y: number }[] = []; const trailLength = randInt(10, 25); return { x, y, targetY, color, speed, size, angle, vx, vy, trail, trailLength, exploded: false, update() { this.trail.push({ x: this.x, y: this.y }); if (this.trail.length > this.trailLength) { this.trail.shift(); } this.x += this.vx; this.y += this.vy; this.vy += 0.02; if (this.vy >= 0 || this.y <= this.targetY) { this.explode(); return false; } return true; }, explode() { const numParticles = randInt(50, 150); const particles: ParticleType[] = []; for (let i = 0; i < numParticles; i++) { const particleAngle = rand(0, Math.PI * 2); const localParticleSpeed = getValueByRange(particleSpeed); const localParticleSize = getValueByRange(particleSize); particles.push( createParticle( this.x, this.y, this.color, localParticleSpeed, particleAngle, 0.05, 0.98, localParticleSize, ), ); } onExplode(particles); }, draw(ctx: CanvasRenderingContext2D) { ctx.save(); ctx.beginPath(); if (this.trail.length > 1) { ctx.moveTo(this.trail[0]?.x ?? this.x, this.trail[0]?.y ?? this.y); for (const point of this.trail) { ctx.lineTo(point.x, point.y); } } else { ctx.moveTo(this.x, this.y); ctx.lineTo(this.x, this.y); } ctx.strokeStyle = this.color; ctx.lineWidth = this.size; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); }, };} function getValueByRange(range: { min: number; max: number } | number): number { if (typeof range === 'number') { return range; } return rand(range.min, range.max);} function getColor(color: string | string[] | undefined): string { if (Array.isArray(color)) { return color[randInt(0, color.length)] ?? randColor(); } return color ?? randColor();} type FireworksBackgroundProps = Omit<React.ComponentProps<'div'>, 'color'> & { canvasProps?: React.ComponentProps<'canvas'>; population?: number; color?: string | string[]; fireworkSpeed?: { min: number; max: number } | number; fireworkSize?: { min: number; max: number } | number; particleSpeed?: { min: number; max: number } | number; particleSize?: { min: number; max: number } | number;}; function FireworksBackground({ ref, className, canvasProps, population = 1, color, fireworkSpeed = { min: 4, max: 8 }, fireworkSize = { min: 2, max: 5 }, particleSpeed = { min: 2, max: 7 }, particleSize = { min: 1, max: 5 }, ...props}: FireworksBackgroundProps) { const canvasRef = React.useRef<HTMLCanvasElement>(null); const containerRef = React.useRef<HTMLDivElement>(null); React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement); React.useEffect(() => { const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container) return; const ctx = canvas.getContext('2d'); if (!ctx) return; let maxX = window.innerWidth; let ratio = container.offsetHeight / container.offsetWidth; let maxY = maxX * ratio; canvas.width = maxX; canvas.height = maxY; const setCanvasSize = () => { maxX = window.innerWidth; ratio = container.offsetHeight / container.offsetWidth; maxY = maxX * ratio; canvas.width = maxX; canvas.height = maxY; }; window.addEventListener('resize', setCanvasSize); const explosions: ParticleType[] = []; const fireworks: FireworkType[] = []; const handleExplosion = (particles: ParticleType[]) => { explosions.push(...particles); }; const launchFirework = () => { const x = rand(maxX * 0.1, maxX * 0.9); const y = maxY; const targetY = rand(maxY * 0.1, maxY * 0.4); const fireworkColor = getColor(color); const speed = getValueByRange(fireworkSpeed); const size = getValueByRange(fireworkSize); fireworks.push( createFirework( x, y, targetY, fireworkColor, speed, size, particleSpeed, particleSize, handleExplosion, ), ); const timeout = rand(300, 800) / population; setTimeout(launchFirework, timeout); }; launchFirework(); let animationFrameId: number; const animate = () => { ctx.clearRect(0, 0, maxX, maxY); for (let i = fireworks.length - 1; i >= 0; i--) { const firework = fireworks[i]; if (!firework?.update()) { fireworks.splice(i, 1); } else { firework.draw(ctx); } } for (let i = explosions.length - 1; i >= 0; i--) { const particle = explosions[i]; particle?.update(); if (particle?.isAlive()) { particle.draw(ctx); } else { explosions.splice(i, 1); } } animationFrameId = requestAnimationFrame(animate); }; animate(); const handleClick = (event: MouseEvent) => { const x = event.clientX; const y = maxY; const targetY = event.clientY; const fireworkColor = getColor(color); const speed = getValueByRange(fireworkSpeed); const size = getValueByRange(fireworkSize); fireworks.push( createFirework( x, y, targetY, fireworkColor, speed, size, particleSpeed, particleSize, handleExplosion, ), ); }; container.addEventListener('click', handleClick); return () => { window.removeEventListener('resize', setCanvasSize); container.removeEventListener('click', handleClick); cancelAnimationFrame(animationFrameId); }; }, [ population, color, fireworkSpeed, fireworkSize, particleSpeed, particleSize, ]); return ( <div ref={containerRef} data-slot="fireworks-background" className={cn('relative size-full overflow-hidden', className)} {...props} > <canvas {...canvasProps} ref={canvasRef} className={cn('absolute inset-0 size-full', canvasProps?.className)} /> </div> );} export { FireworksBackground, type FireworksBackgroundProps };
Built for React applications with TypeScript and Next.js. The animation runs on HTML5 Canvas with requestAnimationFrame
, so you get smooth 60fps explosions. Customize colors, speeds, sizes, and population—from subtle sparkles to full-blown New Year's Eve mayhem.
Installation
npx shadcn@latest add https://www.shadcn.io/registry/fireworks-background.json
npx shadcn@latest add https://www.shadcn.io/registry/fireworks-background.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/fireworks-background.json
bunx shadcn@latest add https://www.shadcn.io/registry/fireworks-background.json
Usage
import { FireworksBackground } from "@/components/ui/fireworks-background";
export default function Celebration() {
return (
<div className="relative h-screen">
<FireworksBackground
population={3}
color={["#ff0000", "#00ff00", "#0000ff", "#ffff00"]}
fireworkSpeed={{min: 4, max: 8}}
particleSize={{min: 2, max: 6}}
/>
<div className="relative z-10 text-white p-8">
<h1>Click anywhere to celebrate!</h1>
</div>
</div>
);
}
Why most firework animations look fake
Most developers use pre-made GIFs or simple CSS animations. Static, repetitive, no physics. Some try particle libraries but end up with floating dots that ignore gravity. Real fireworks have weight—they arc up, explode at peak height, then fall down with particle trails.
This React component simulates actual physics. Fireworks launch with initial velocity, slow down due to gravity, explode at peak trajectory, then particles scatter with momentum and fade with realistic decay. Click anywhere and the firework aims for that spot. Canvas rendering keeps it smooth even with hundreds of particles exploding simultaneously.
Examples
High Population
More frequent launches for constant celebration:
'use client'; import { FireworksBackground } from '@/components/ui/shadcn-io/fireworks-background'; const FireworksBackgroundPopulationDemo = () => { return ( <FireworksBackground className="absolute inset-0 flex items-center justify-center rounded-xl" population={8} /> );};export default FireworksBackgroundPopulationDemo;
'use client'; import * as React from 'react'; import { cn } from '@/lib/utils'; const rand = (min: number, max: number): number => Math.random() * (max - min) + min; const randInt = (min: number, max: number): number => Math.floor(Math.random() * (max - min) + min); const randColor = (): string => `hsl(${randInt(0, 360)}, 100%, 50%)`; type ParticleType = { x: number; y: number; color: string; speed: number; direction: number; vx: number; vy: number; gravity: number; friction: number; alpha: number; decay: number; size: number; update: () => void; draw: (ctx: CanvasRenderingContext2D) => void; isAlive: () => boolean;}; function createParticle( x: number, y: number, color: string, speed: number, direction: number, gravity: number, friction: number, size: number,): ParticleType { const vx = Math.cos(direction) * speed; const vy = Math.sin(direction) * speed; const alpha = 1; const decay = rand(0.005, 0.02); return { x, y, color, speed, direction, vx, vy, gravity, friction, alpha, decay, size, update() { this.vx *= this.friction; this.vy *= this.friction; this.vy += this.gravity; this.x += this.vx; this.y += this.vy; this.alpha -= this.decay; }, draw(ctx: CanvasRenderingContext2D) { ctx.save(); ctx.globalAlpha = this.alpha; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fillStyle = this.color; ctx.fill(); ctx.restore(); }, isAlive() { return this.alpha > 0; }, };} type FireworkType = { x: number; y: number; targetY: number; color: string; speed: number; size: number; angle: number; vx: number; vy: number; trail: { x: number; y: number }[]; trailLength: number; exploded: boolean; update: () => boolean; explode: () => void; draw: (ctx: CanvasRenderingContext2D) => void;}; function createFirework( x: number, y: number, targetY: number, color: string, speed: number, size: number, particleSpeed: { min: number; max: number } | number, particleSize: { min: number; max: number } | number, onExplode: (particles: ParticleType[]) => void,): FireworkType { const angle = -Math.PI / 2 + rand(-0.3, 0.3); const vx = Math.cos(angle) * speed; const vy = Math.sin(angle) * speed; const trail: { x: number; y: number }[] = []; const trailLength = randInt(10, 25); return { x, y, targetY, color, speed, size, angle, vx, vy, trail, trailLength, exploded: false, update() { this.trail.push({ x: this.x, y: this.y }); if (this.trail.length > this.trailLength) { this.trail.shift(); } this.x += this.vx; this.y += this.vy; this.vy += 0.02; if (this.vy >= 0 || this.y <= this.targetY) { this.explode(); return false; } return true; }, explode() { const numParticles = randInt(50, 150); const particles: ParticleType[] = []; for (let i = 0; i < numParticles; i++) { const particleAngle = rand(0, Math.PI * 2); const localParticleSpeed = getValueByRange(particleSpeed); const localParticleSize = getValueByRange(particleSize); particles.push( createParticle( this.x, this.y, this.color, localParticleSpeed, particleAngle, 0.05, 0.98, localParticleSize, ), ); } onExplode(particles); }, draw(ctx: CanvasRenderingContext2D) { ctx.save(); ctx.beginPath(); if (this.trail.length > 1) { ctx.moveTo(this.trail[0]?.x ?? this.x, this.trail[0]?.y ?? this.y); for (const point of this.trail) { ctx.lineTo(point.x, point.y); } } else { ctx.moveTo(this.x, this.y); ctx.lineTo(this.x, this.y); } ctx.strokeStyle = this.color; ctx.lineWidth = this.size; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); }, };} function getValueByRange(range: { min: number; max: number } | number): number { if (typeof range === 'number') { return range; } return rand(range.min, range.max);} function getColor(color: string | string[] | undefined): string { if (Array.isArray(color)) { return color[randInt(0, color.length)] ?? randColor(); } return color ?? randColor();} type FireworksBackgroundProps = Omit<React.ComponentProps<'div'>, 'color'> & { canvasProps?: React.ComponentProps<'canvas'>; population?: number; color?: string | string[]; fireworkSpeed?: { min: number; max: number } | number; fireworkSize?: { min: number; max: number } | number; particleSpeed?: { min: number; max: number } | number; particleSize?: { min: number; max: number } | number;}; function FireworksBackground({ ref, className, canvasProps, population = 1, color, fireworkSpeed = { min: 4, max: 8 }, fireworkSize = { min: 2, max: 5 }, particleSpeed = { min: 2, max: 7 }, particleSize = { min: 1, max: 5 }, ...props}: FireworksBackgroundProps) { const canvasRef = React.useRef<HTMLCanvasElement>(null); const containerRef = React.useRef<HTMLDivElement>(null); React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement); React.useEffect(() => { const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container) return; const ctx = canvas.getContext('2d'); if (!ctx) return; let maxX = window.innerWidth; let ratio = container.offsetHeight / container.offsetWidth; let maxY = maxX * ratio; canvas.width = maxX; canvas.height = maxY; const setCanvasSize = () => { maxX = window.innerWidth; ratio = container.offsetHeight / container.offsetWidth; maxY = maxX * ratio; canvas.width = maxX; canvas.height = maxY; }; window.addEventListener('resize', setCanvasSize); const explosions: ParticleType[] = []; const fireworks: FireworkType[] = []; const handleExplosion = (particles: ParticleType[]) => { explosions.push(...particles); }; const launchFirework = () => { const x = rand(maxX * 0.1, maxX * 0.9); const y = maxY; const targetY = rand(maxY * 0.1, maxY * 0.4); const fireworkColor = getColor(color); const speed = getValueByRange(fireworkSpeed); const size = getValueByRange(fireworkSize); fireworks.push( createFirework( x, y, targetY, fireworkColor, speed, size, particleSpeed, particleSize, handleExplosion, ), ); const timeout = rand(300, 800) / population; setTimeout(launchFirework, timeout); }; launchFirework(); let animationFrameId: number; const animate = () => { ctx.clearRect(0, 0, maxX, maxY); for (let i = fireworks.length - 1; i >= 0; i--) { const firework = fireworks[i]; if (!firework?.update()) { fireworks.splice(i, 1); } else { firework.draw(ctx); } } for (let i = explosions.length - 1; i >= 0; i--) { const particle = explosions[i]; particle?.update(); if (particle?.isAlive()) { particle.draw(ctx); } else { explosions.splice(i, 1); } } animationFrameId = requestAnimationFrame(animate); }; animate(); const handleClick = (event: MouseEvent) => { const x = event.clientX; const y = maxY; const targetY = event.clientY; const fireworkColor = getColor(color); const speed = getValueByRange(fireworkSpeed); const size = getValueByRange(fireworkSize); fireworks.push( createFirework( x, y, targetY, fireworkColor, speed, size, particleSpeed, particleSize, handleExplosion, ), ); }; container.addEventListener('click', handleClick); return () => { window.removeEventListener('resize', setCanvasSize); container.removeEventListener('click', handleClick); cancelAnimationFrame(animationFrameId); }; }, [ population, color, fireworkSpeed, fireworkSize, particleSpeed, particleSize, ]); return ( <div ref={containerRef} data-slot="fireworks-background" className={cn('relative size-full overflow-hidden', className)} {...props} > <canvas {...canvasProps} ref={canvasRef} className={cn('absolute inset-0 size-full', canvasProps?.className)} /> </div> );} export { FireworksBackground, type FireworksBackgroundProps };
Variable Sizes and Speeds
Dynamic range parameters for organic, unpredictable effects:
'use client'; import { FireworksBackground } from '@/components/ui/shadcn-io/fireworks-background'; const FireworksBackgroundSizeSpeedDemo = () => { return ( <FireworksBackground className="absolute inset-0 flex items-center justify-center rounded-xl" fireworkSpeed={{ min: 8, max: 16 }} fireworkSize={{ min: 4, max: 10 }} particleSpeed={{ min: 4, max: 14 }} particleSize={{ min: 2, max: 10 }} /> );};export default FireworksBackgroundSizeSpeedDemo;
'use client'; import * as React from 'react'; import { cn } from '@/lib/utils'; const rand = (min: number, max: number): number => Math.random() * (max - min) + min; const randInt = (min: number, max: number): number => Math.floor(Math.random() * (max - min) + min); const randColor = (): string => `hsl(${randInt(0, 360)}, 100%, 50%)`; type ParticleType = { x: number; y: number; color: string; speed: number; direction: number; vx: number; vy: number; gravity: number; friction: number; alpha: number; decay: number; size: number; update: () => void; draw: (ctx: CanvasRenderingContext2D) => void; isAlive: () => boolean;}; function createParticle( x: number, y: number, color: string, speed: number, direction: number, gravity: number, friction: number, size: number,): ParticleType { const vx = Math.cos(direction) * speed; const vy = Math.sin(direction) * speed; const alpha = 1; const decay = rand(0.005, 0.02); return { x, y, color, speed, direction, vx, vy, gravity, friction, alpha, decay, size, update() { this.vx *= this.friction; this.vy *= this.friction; this.vy += this.gravity; this.x += this.vx; this.y += this.vy; this.alpha -= this.decay; }, draw(ctx: CanvasRenderingContext2D) { ctx.save(); ctx.globalAlpha = this.alpha; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fillStyle = this.color; ctx.fill(); ctx.restore(); }, isAlive() { return this.alpha > 0; }, };} type FireworkType = { x: number; y: number; targetY: number; color: string; speed: number; size: number; angle: number; vx: number; vy: number; trail: { x: number; y: number }[]; trailLength: number; exploded: boolean; update: () => boolean; explode: () => void; draw: (ctx: CanvasRenderingContext2D) => void;}; function createFirework( x: number, y: number, targetY: number, color: string, speed: number, size: number, particleSpeed: { min: number; max: number } | number, particleSize: { min: number; max: number } | number, onExplode: (particles: ParticleType[]) => void,): FireworkType { const angle = -Math.PI / 2 + rand(-0.3, 0.3); const vx = Math.cos(angle) * speed; const vy = Math.sin(angle) * speed; const trail: { x: number; y: number }[] = []; const trailLength = randInt(10, 25); return { x, y, targetY, color, speed, size, angle, vx, vy, trail, trailLength, exploded: false, update() { this.trail.push({ x: this.x, y: this.y }); if (this.trail.length > this.trailLength) { this.trail.shift(); } this.x += this.vx; this.y += this.vy; this.vy += 0.02; if (this.vy >= 0 || this.y <= this.targetY) { this.explode(); return false; } return true; }, explode() { const numParticles = randInt(50, 150); const particles: ParticleType[] = []; for (let i = 0; i < numParticles; i++) { const particleAngle = rand(0, Math.PI * 2); const localParticleSpeed = getValueByRange(particleSpeed); const localParticleSize = getValueByRange(particleSize); particles.push( createParticle( this.x, this.y, this.color, localParticleSpeed, particleAngle, 0.05, 0.98, localParticleSize, ), ); } onExplode(particles); }, draw(ctx: CanvasRenderingContext2D) { ctx.save(); ctx.beginPath(); if (this.trail.length > 1) { ctx.moveTo(this.trail[0]?.x ?? this.x, this.trail[0]?.y ?? this.y); for (const point of this.trail) { ctx.lineTo(point.x, point.y); } } else { ctx.moveTo(this.x, this.y); ctx.lineTo(this.x, this.y); } ctx.strokeStyle = this.color; ctx.lineWidth = this.size; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); }, };} function getValueByRange(range: { min: number; max: number } | number): number { if (typeof range === 'number') { return range; } return rand(range.min, range.max);} function getColor(color: string | string[] | undefined): string { if (Array.isArray(color)) { return color[randInt(0, color.length)] ?? randColor(); } return color ?? randColor();} type FireworksBackgroundProps = Omit<React.ComponentProps<'div'>, 'color'> & { canvasProps?: React.ComponentProps<'canvas'>; population?: number; color?: string | string[]; fireworkSpeed?: { min: number; max: number } | number; fireworkSize?: { min: number; max: number } | number; particleSpeed?: { min: number; max: number } | number; particleSize?: { min: number; max: number } | number;}; function FireworksBackground({ ref, className, canvasProps, population = 1, color, fireworkSpeed = { min: 4, max: 8 }, fireworkSize = { min: 2, max: 5 }, particleSpeed = { min: 2, max: 7 }, particleSize = { min: 1, max: 5 }, ...props}: FireworksBackgroundProps) { const canvasRef = React.useRef<HTMLCanvasElement>(null); const containerRef = React.useRef<HTMLDivElement>(null); React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement); React.useEffect(() => { const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container) return; const ctx = canvas.getContext('2d'); if (!ctx) return; let maxX = window.innerWidth; let ratio = container.offsetHeight / container.offsetWidth; let maxY = maxX * ratio; canvas.width = maxX; canvas.height = maxY; const setCanvasSize = () => { maxX = window.innerWidth; ratio = container.offsetHeight / container.offsetWidth; maxY = maxX * ratio; canvas.width = maxX; canvas.height = maxY; }; window.addEventListener('resize', setCanvasSize); const explosions: ParticleType[] = []; const fireworks: FireworkType[] = []; const handleExplosion = (particles: ParticleType[]) => { explosions.push(...particles); }; const launchFirework = () => { const x = rand(maxX * 0.1, maxX * 0.9); const y = maxY; const targetY = rand(maxY * 0.1, maxY * 0.4); const fireworkColor = getColor(color); const speed = getValueByRange(fireworkSpeed); const size = getValueByRange(fireworkSize); fireworks.push( createFirework( x, y, targetY, fireworkColor, speed, size, particleSpeed, particleSize, handleExplosion, ), ); const timeout = rand(300, 800) / population; setTimeout(launchFirework, timeout); }; launchFirework(); let animationFrameId: number; const animate = () => { ctx.clearRect(0, 0, maxX, maxY); for (let i = fireworks.length - 1; i >= 0; i--) { const firework = fireworks[i]; if (!firework?.update()) { fireworks.splice(i, 1); } else { firework.draw(ctx); } } for (let i = explosions.length - 1; i >= 0; i--) { const particle = explosions[i]; particle?.update(); if (particle?.isAlive()) { particle.draw(ctx); } else { explosions.splice(i, 1); } } animationFrameId = requestAnimationFrame(animate); }; animate(); const handleClick = (event: MouseEvent) => { const x = event.clientX; const y = maxY; const targetY = event.clientY; const fireworkColor = getColor(color); const speed = getValueByRange(fireworkSpeed); const size = getValueByRange(fireworkSize); fireworks.push( createFirework( x, y, targetY, fireworkColor, speed, size, particleSpeed, particleSize, handleExplosion, ), ); }; container.addEventListener('click', handleClick); return () => { window.removeEventListener('resize', setCanvasSize); container.removeEventListener('click', handleClick); cancelAnimationFrame(animationFrameId); }; }, [ population, color, fireworkSpeed, fireworkSize, particleSpeed, particleSize, ]); return ( <div ref={containerRef} data-slot="fireworks-background" className={cn('relative size-full overflow-hidden', className)} {...props} > <canvas {...canvasProps} ref={canvasRef} className={cn('absolute inset-0 size-full', canvasProps?.className)} /> </div> );} export { FireworksBackground, type FireworksBackgroundProps };
Fixed Parameters
Consistent sizing for uniform, controlled patterns:
'use client'; import { FireworksBackground } from '@/components/ui/shadcn-io/fireworks-background'; const FireworksBackgroundFixedSizeSpeedDemo = () => { return ( <FireworksBackground className="absolute inset-0 flex items-center justify-center rounded-xl" fireworkSize={7} fireworkSpeed={7} particleSize={7} particleSpeed={7} /> );};export default FireworksBackgroundFixedSizeSpeedDemo;
'use client'; import * as React from 'react'; import { cn } from '@/lib/utils'; const rand = (min: number, max: number): number => Math.random() * (max - min) + min; const randInt = (min: number, max: number): number => Math.floor(Math.random() * (max - min) + min); const randColor = (): string => `hsl(${randInt(0, 360)}, 100%, 50%)`; type ParticleType = { x: number; y: number; color: string; speed: number; direction: number; vx: number; vy: number; gravity: number; friction: number; alpha: number; decay: number; size: number; update: () => void; draw: (ctx: CanvasRenderingContext2D) => void; isAlive: () => boolean;}; function createParticle( x: number, y: number, color: string, speed: number, direction: number, gravity: number, friction: number, size: number,): ParticleType { const vx = Math.cos(direction) * speed; const vy = Math.sin(direction) * speed; const alpha = 1; const decay = rand(0.005, 0.02); return { x, y, color, speed, direction, vx, vy, gravity, friction, alpha, decay, size, update() { this.vx *= this.friction; this.vy *= this.friction; this.vy += this.gravity; this.x += this.vx; this.y += this.vy; this.alpha -= this.decay; }, draw(ctx: CanvasRenderingContext2D) { ctx.save(); ctx.globalAlpha = this.alpha; ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fillStyle = this.color; ctx.fill(); ctx.restore(); }, isAlive() { return this.alpha > 0; }, };} type FireworkType = { x: number; y: number; targetY: number; color: string; speed: number; size: number; angle: number; vx: number; vy: number; trail: { x: number; y: number }[]; trailLength: number; exploded: boolean; update: () => boolean; explode: () => void; draw: (ctx: CanvasRenderingContext2D) => void;}; function createFirework( x: number, y: number, targetY: number, color: string, speed: number, size: number, particleSpeed: { min: number; max: number } | number, particleSize: { min: number; max: number } | number, onExplode: (particles: ParticleType[]) => void,): FireworkType { const angle = -Math.PI / 2 + rand(-0.3, 0.3); const vx = Math.cos(angle) * speed; const vy = Math.sin(angle) * speed; const trail: { x: number; y: number }[] = []; const trailLength = randInt(10, 25); return { x, y, targetY, color, speed, size, angle, vx, vy, trail, trailLength, exploded: false, update() { this.trail.push({ x: this.x, y: this.y }); if (this.trail.length > this.trailLength) { this.trail.shift(); } this.x += this.vx; this.y += this.vy; this.vy += 0.02; if (this.vy >= 0 || this.y <= this.targetY) { this.explode(); return false; } return true; }, explode() { const numParticles = randInt(50, 150); const particles: ParticleType[] = []; for (let i = 0; i < numParticles; i++) { const particleAngle = rand(0, Math.PI * 2); const localParticleSpeed = getValueByRange(particleSpeed); const localParticleSize = getValueByRange(particleSize); particles.push( createParticle( this.x, this.y, this.color, localParticleSpeed, particleAngle, 0.05, 0.98, localParticleSize, ), ); } onExplode(particles); }, draw(ctx: CanvasRenderingContext2D) { ctx.save(); ctx.beginPath(); if (this.trail.length > 1) { ctx.moveTo(this.trail[0]?.x ?? this.x, this.trail[0]?.y ?? this.y); for (const point of this.trail) { ctx.lineTo(point.x, point.y); } } else { ctx.moveTo(this.x, this.y); ctx.lineTo(this.x, this.y); } ctx.strokeStyle = this.color; ctx.lineWidth = this.size; ctx.lineCap = 'round'; ctx.stroke(); ctx.restore(); }, };} function getValueByRange(range: { min: number; max: number } | number): number { if (typeof range === 'number') { return range; } return rand(range.min, range.max);} function getColor(color: string | string[] | undefined): string { if (Array.isArray(color)) { return color[randInt(0, color.length)] ?? randColor(); } return color ?? randColor();} type FireworksBackgroundProps = Omit<React.ComponentProps<'div'>, 'color'> & { canvasProps?: React.ComponentProps<'canvas'>; population?: number; color?: string | string[]; fireworkSpeed?: { min: number; max: number } | number; fireworkSize?: { min: number; max: number } | number; particleSpeed?: { min: number; max: number } | number; particleSize?: { min: number; max: number } | number;}; function FireworksBackground({ ref, className, canvasProps, population = 1, color, fireworkSpeed = { min: 4, max: 8 }, fireworkSize = { min: 2, max: 5 }, particleSpeed = { min: 2, max: 7 }, particleSize = { min: 1, max: 5 }, ...props}: FireworksBackgroundProps) { const canvasRef = React.useRef<HTMLCanvasElement>(null); const containerRef = React.useRef<HTMLDivElement>(null); React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement); React.useEffect(() => { const canvas = canvasRef.current; const container = containerRef.current; if (!canvas || !container) return; const ctx = canvas.getContext('2d'); if (!ctx) return; let maxX = window.innerWidth; let ratio = container.offsetHeight / container.offsetWidth; let maxY = maxX * ratio; canvas.width = maxX; canvas.height = maxY; const setCanvasSize = () => { maxX = window.innerWidth; ratio = container.offsetHeight / container.offsetWidth; maxY = maxX * ratio; canvas.width = maxX; canvas.height = maxY; }; window.addEventListener('resize', setCanvasSize); const explosions: ParticleType[] = []; const fireworks: FireworkType[] = []; const handleExplosion = (particles: ParticleType[]) => { explosions.push(...particles); }; const launchFirework = () => { const x = rand(maxX * 0.1, maxX * 0.9); const y = maxY; const targetY = rand(maxY * 0.1, maxY * 0.4); const fireworkColor = getColor(color); const speed = getValueByRange(fireworkSpeed); const size = getValueByRange(fireworkSize); fireworks.push( createFirework( x, y, targetY, fireworkColor, speed, size, particleSpeed, particleSize, handleExplosion, ), ); const timeout = rand(300, 800) / population; setTimeout(launchFirework, timeout); }; launchFirework(); let animationFrameId: number; const animate = () => { ctx.clearRect(0, 0, maxX, maxY); for (let i = fireworks.length - 1; i >= 0; i--) { const firework = fireworks[i]; if (!firework?.update()) { fireworks.splice(i, 1); } else { firework.draw(ctx); } } for (let i = explosions.length - 1; i >= 0; i--) { const particle = explosions[i]; particle?.update(); if (particle?.isAlive()) { particle.draw(ctx); } else { explosions.splice(i, 1); } } animationFrameId = requestAnimationFrame(animate); }; animate(); const handleClick = (event: MouseEvent) => { const x = event.clientX; const y = maxY; const targetY = event.clientY; const fireworkColor = getColor(color); const speed = getValueByRange(fireworkSpeed); const size = getValueByRange(fireworkSize); fireworks.push( createFirework( x, y, targetY, fireworkColor, speed, size, particleSpeed, particleSize, handleExplosion, ), ); }; container.addEventListener('click', handleClick); return () => { window.removeEventListener('resize', setCanvasSize); container.removeEventListener('click', handleClick); cancelAnimationFrame(animationFrameId); }; }, [ population, color, fireworkSpeed, fireworkSize, particleSpeed, particleSize, ]); return ( <div ref={containerRef} data-slot="fireworks-background" className={cn('relative size-full overflow-hidden', className)} {...props} > <canvas {...canvasProps} ref={canvasRef} className={cn('absolute inset-0 size-full', canvasProps?.className)} /> </div> );} export { FireworksBackground, type FireworksBackgroundProps };
Features
- Click-to-launch interactivity aiming fireworks toward cursor position
- Realistic physics simulation with gravity, momentum, and particle decay
- Configurable parameters for population, colors, speeds, and sizes
- Range or fixed values supporting both random and consistent effects
- Canvas-based rendering with smooth 60fps animations
- TypeScript definitions for all configuration options
- Automatic cleanup preventing memory leaks on unmount
- shadcn/ui compatible with responsive container sizing
API Reference
FireworksBackground
Main component for the fireworks animation canvas.
Prop | Type | Default | Description |
---|---|---|---|
population | number | 1 | Launch frequency (higher = more fireworks) |
color | string | string[] | randColor() | Single color or array for fireworks |
fireworkSpeed | {min: number, max: number} | number | {min: 4, max: 8} | Launch velocity range or fixed speed |
fireworkSize | {min: number, max: number} | number | {min: 2, max: 5} | Trail thickness range or fixed size |
particleSpeed | {min: number, max: number} | number | {min: 2, max: 7} | Explosion particle speed range |
particleSize | {min: number, max: number} | number | {min: 1, max: 5} | Explosion particle size range |
canvasProps | ComponentProps<'canvas'> | - | Additional canvas element props |
Parameter Types
// Range for randomized values
type Range = { min: number; max: number };
// Fixed value for consistent effects
type Fixed = number;
// Color options
type Color = string | string[];
Common gotchas
Canvas sizing issues: The canvas auto-resizes to its container. If fireworks aren't showing, check that the parent has explicit dimensions like h-screen
.
Performance with high population: Each firework spawns dozens of particles. Population above 5 might lag on weak devices. Test on target hardware.
Click detection problems: Click handlers are on the canvas element. Z-index conflicts with overlays can block interaction. Ensure the canvas is clickable.
Memory leaks with rapid mounting: The component cleans up requestAnimationFrame
automatically, but rapidly changing props might cause issues. Use stable keys when possible.
Colors not showing: Color arrays need valid CSS color strings. Hex, RGB, and named colors all work. Invalid colors fall back to random generation.
You might also like
Explore other celebratory background components for React applications:
Sparkles
Twinkling particle effects for magical celebrations
Particles
Interactive particle systems with mouse following
Beams with Collision
Energy beams that explode into particles on impact
Meteors
Falling star animations for celestial themes
Vortex
Swirling particle effects with mesmerizing depth
Shooting Stars
Streaking star trails with customizable paths
Questions developers actually ask
Ethereal Shadow Background
React atmospheric shadow background with breathing fog effects. Cinematic SVG filters and noise textures with TypeScript and shadcn/ui.
React Flickering Grid Background
React flickering grid background with CRT monitor effects. Canvas-based subtle visual noise animations using TypeScript and shadcn/ui styling.