Fireworks Background
Interactive canvas-based fireworks animation with customizable particles and click-to-launch functionality. Perfect for React applications requiring celebratory backgrounds with Next.js integration and TypeScript support.
Powered by
'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 };
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
Features
- Canvas-based animations with smooth particle physics and realistic firework trajectories
- Interactive click-to-launch allowing users to create fireworks by clicking anywhere on the canvas
- Customizable parameters including population density, colors, speeds, and particle sizes
- Automatic launching with configurable timing and random positioning for continuous displays
- TypeScript support with complete interface definitions and flexible configuration options
- shadcn/ui compatibility using utility classes and responsive design patterns
Examples
High Population
'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 };
Demonstrates increased firework frequency with higher population setting.
Variable Sizes and Speeds
'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 };
Shows customizable size and speed ranges for more dynamic effects.
Fixed Parameters
'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 };
Uses consistent sizing and speed values for uniform firework patterns.
Use Cases
This free open source React component works well for:
- Celebration pages - New Year, achievements, and milestone backgrounds built with Next.js
- Event websites - Festival and party themes using TypeScript and Tailwind CSS
- Gaming interfaces - Victory screens and achievement unlocks with interactive elements
- Marketing campaigns - Product launches and promotional backgrounds
API Reference
FireworksBackground
Prop | Type | Default | Description |
---|---|---|---|
population | number | 1 | Controls firework launch frequency (higher = more frequent) |
color | string | string[] | randColor() | Single color or array of colors for fireworks |
fireworkSpeed | {min: number, max: number} | number | {min: 4, max: 8} | Launch speed range or fixed speed |
fireworkSize | {min: number, max: number} | number | {min: 2, max: 5} | Firework trail thickness range or fixed size |
particleSpeed | {min: number, max: number} | number | {min: 2, max: 7} | Explosion particle speed range or fixed speed |
particleSize | {min: number, max: number} | number | {min: 1, max: 5} | Explosion particle size range or fixed size |
canvasProps | React.ComponentProps<'canvas'> | - | Additional props passed to the canvas element |
Configuration Options
- Range values - Use
{min: number, max: number}
for randomized effects - Fixed values - Use
number
for consistent, uniform appearance - Colors - Support single strings, arrays of colors, or automatic random colors
Implementation Notes
- Canvas automatically resizes to match container dimensions
- Physics simulation includes gravity, friction, and particle decay
- Click handling launches fireworks toward cursor position
- Automatic cleanup prevents memory leaks during component unmount
- Compatible with shadcn/ui theming and responsive layouts
- Optimized performance with requestAnimationFrame animations
Etheral Shadow
Animated shadow overlay effects with SVG filters and noise textures. Perfect for React applications requiring atmospheric backgrounds with Next.js integration and TypeScript support.
Flickering Grid
Canvas-based flickering grid animations with performance optimization. Perfect for React applications requiring subtle animated backgrounds with Next.js integration and TypeScript support.