Fireworks

PreviousNext

Add animated fireworks to your site background with this festive component. Perfect for celebrations. Built with Tailwind CSS and Framer Motion.

Docs
bunduicomponent

Preview

Loading preview…
examples/motion/backgrounds/fireworks/01/fireworks.tsx
"use client";

import React, { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";

interface Particle {
  x: number;
  y: number;
  color: string;
  velocity: {
    x: number;
    y: number;
  };
  alpha: number;
  lifetime: number;
  size: number;
}

interface Firework {
  x: number;
  y: number;
  color: string;
  velocity: {
    x: number;
    y: number;
  };
  particles: Particle[];
  exploded: boolean;
  timeToExplode: number;
}

function FireworksBackground({
  children,
  className
}: {
  children: React.ReactNode;
  className?: string;
}) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const fireworksRef = useRef<Firework[]>([]);
  const animationFrameRef = useRef<number>(0);
  const lastFireworkTimeRef = useRef<number>(Date.now());

  const isDarkModeRef = useRef<boolean>(false);

  const colors = ["#9b87f5", "#D946EF", "#F97316", "#0EA5E9", "#ea384c", "#10B981", "#FCD34D"];

  const createFirework = (x?: number, y?: number, targetY?: number) => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const startX = x || Math.random() * canvas.width;
    const startY = canvas.height;
    const color = colors[Math.floor(Math.random() * colors.length)];
    const angle = (Math.random() * Math.PI) / 2 - Math.PI / 4;
    const velocity = 6 + Math.random() * 4;

    const target = targetY || canvas.height * (0.1 + Math.random() * 0.4);

    const firework: Firework = {
      x: startX,
      y: startY,
      color,
      velocity: {
        x: Math.sin(angle) * velocity,
        y: -Math.cos(angle) * velocity * 1.5
      },
      particles: [],
      exploded: false,
      timeToExplode: target
    };

    fireworksRef.current.push(firework);
  };

  const explodeFirework = (firework: Firework) => {
    const particleCount = 60 + Math.floor(Math.random() * 40);

    for (let i = 0; i < particleCount; i++) {
      const angle = Math.random() * Math.PI * 2;
      const velocity = Math.random() * 5 + 1;

      firework.particles.push({
        x: firework.x,
        y: firework.y,
        color: firework.color,
        velocity: {
          x: Math.cos(angle) * velocity * (0.5 + Math.random()),
          y: Math.sin(angle) * velocity * (0.5 + Math.random())
        },
        alpha: 1,
        lifetime: Math.random() * 30 + 30,
        size: Math.random() * 3 + 1
      });
    }
  };

  const updateAndDraw = () => {
    const canvas = canvasRef.current;
    const ctx = canvas?.getContext("2d");

    if (!canvas || !ctx) return;

    const fillStyle = isDarkModeRef.current ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.1)";

    ctx.fillStyle = fillStyle;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    const currentFireworks = fireworksRef.current;
    for (let i = 0; i < currentFireworks.length; i++) {
      const firework = currentFireworks[i];

      if (!firework.exploded) {
        firework.x += firework.velocity.x;
        firework.y += firework.velocity.y;
        firework.velocity.y += 0.1;

        ctx.beginPath();
        ctx.arc(firework.x, firework.y, 3, 0, Math.PI * 2);
        ctx.fillStyle = firework.color;
        ctx.fill();

        if (
          firework.y <= firework.timeToExplode ||
          firework.velocity.y >= 0 ||
          firework.x < 0 ||
          firework.x > canvas.width
        ) {
          if (firework.y > 0 && firework.y < canvas.height) {
            explodeFirework(firework);
          }

          firework.exploded = true;
        }
      } else {
        for (let j = 0; j < firework.particles.length; j++) {
          const particle = firework.particles[j];

          particle.x += particle.velocity.x;
          particle.y += particle.velocity.y;
          particle.velocity.y += 0.05;
          particle.alpha -= 1 / particle.lifetime;

          if (particle.alpha <= 0.1) {
            firework.particles.splice(j, 1);
            j--;
            continue;
          }

          ctx.globalAlpha = particle.alpha;
          ctx.beginPath();
          ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
          ctx.fillStyle = particle.color;
          ctx.fill();
          ctx.globalAlpha = 1;
        }

        if (firework.particles.length === 0) {
          currentFireworks.splice(i, 1);
          i--;
        }
      }
    }

    const now = Date.now();
    if (now - lastFireworkTimeRef.current > 1000 + Math.random() * 2000) {
      const numberOfFireworks = Math.floor(Math.random() * 2) + 1;
      for (let i = 0; i < numberOfFireworks; i++) {
        createFirework();
      }
      lastFireworkTimeRef.current = now;
    }

    animationFrameRef.current = requestAnimationFrame(updateAndDraw);
  };

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const updateCanvasSize = () => {
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
    };

    updateCanvasSize();
    window.addEventListener("resize", updateCanvasSize);

    const checkDarkMode = () => {
      const htmlElement = document.documentElement;
      const isDarkClass = htmlElement.classList.contains("dark");

      const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;

      isDarkModeRef.current = isDarkClass || prefersDark;
    };

    const mediaQueryListener = (e: MediaQueryListEvent) => {
      isDarkModeRef.current = e.matches;
    };

    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    mediaQuery.addEventListener("change", mediaQueryListener);

    checkDarkMode();

    for (let i = 0; i < 3; i++) {
      createFirework();
    }
    lastFireworkTimeRef.current = Date.now();

    animationFrameRef.current = requestAnimationFrame(updateAndDraw);

    return () => {
      if (animationFrameRef.current) {
        cancelAnimationFrame(animationFrameRef.current);
      }
      window.removeEventListener("resize", updateCanvasSize);
      mediaQuery.removeEventListener("change", mediaQueryListener);
    };
  }, []);

  return (
    <div className={cn("relative w-full", className)}>
      <canvas ref={canvasRef} className="absolute inset-0 h-full w-full" />
      {children}
    </div>
  );
}

export default FireworksBackground;

Installation

npx shadcn@latest add @bundui/fireworks

Usage

import { Fireworks } from "@/components/fireworks"
<Fireworks />