Particles Background

A component with particle background pattern

Example

Particles Background

Installation

Copy and paste the source code into your project.

"use client";
 
import React, { useCallback, useEffect, useRef } from "react";
import { useTheme } from "next-themes";
 
function hexToRgb(hex: string): number[] {
  hex = hex.replace("#", "");
 
  if (hex.length === 3) {
    hex = hex
      .split("")
      .map((char) => char + char)
      .join("");
  }
 
  const hexInt = parseInt(hex, 16);
  const red = (hexInt >> 16) & 255;
  const green = (hexInt >> 8) & 255;
  const blue = hexInt & 255;
  return [red, green, blue];
}
 
interface ParticlesProps {
  quantity?: number;
  size?: number;
  color?: string;
  darkModeColor?: string;
  speed?: number;
  children?: React.ReactNode;
}
/**
 *
 * ParticlesBackground is a component that renders a canvas element with particles that move upwards.
 *
 *
 * @param {number} [props.quantity=200] - The number of particles to render.
 * @param {number} [props.size=0.01] - The base size of each particle.
 * @param {string} [props.color="#000000"] - The color of particles in light mode (hex format).
 * @param {string} [props.darkModeColor="#ffffff"] - The color of particles in dark mode (hex format).
 * @param {number} [props.speed=0.1] - The speed at which particles move upwards.
 * @param {React.ReactNode} [props.children] - Optional child elements to be wrapped by the Particles component.
 *
 */
export const ParticlesBackground: React.FC<ParticlesProps> = ({
  quantity = 200,
  size = 0.01,
  color = "#000000",
  darkModeColor = "#ffffff",
  speed = 0.1,
  children,
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const context = useRef<CanvasRenderingContext2D | null>(null);
  const circles = useRef<any[]>([]);
  const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 });
  const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1;
 
  const { theme } = useTheme();
 
  const particleColor = theme !== "dark" ? color : darkModeColor;
  const rgb = hexToRgb(particleColor);
 
  type Circle = {
    x: number;
    y: number;
    size: number;
    alpha: number;
    fadeInStartTime: number;
    fadeSpeed: number;
    dx: number;
    dy: number;
  };
 
  const resizeCanvas = useCallback(() => {
    if (canvasRef.current && context.current) {
      circles.current.length = 0;
      canvasSize.current.w = window.innerWidth;
      canvasSize.current.h = window.innerHeight;
      canvasRef.current.width = canvasSize.current.w * dpr;
      canvasRef.current.height = canvasSize.current.h * dpr;
      canvasRef.current.style.width = `${canvasSize.current.w}px`;
      canvasRef.current.style.height = `${canvasSize.current.h}px`;
      context.current.scale(dpr, dpr);
    }
  }, [dpr]);
 
  const circleParams = useCallback((): Circle => {
    const x = Math.floor(Math.random() * canvasSize.current.w);
    const y = Math.floor(Math.random() * canvasSize.current.h);
    const pSize = Math.floor(Math.random() * 2) + size;
    const alpha = 0;
    const dx = (Math.random() - 0.5) * 0.01;
    const dy = (Math.random() - 0.5) * 0.1;
    const fadeInStartTime = Date.now();
    const fadeSpeed = Math.random() * 0.0009 + 0.0006;
    return {
      x,
      y,
      size: pSize,
      alpha,
      dx,
      dy,
      fadeInStartTime,
      fadeSpeed,
    };
  }, [canvasSize, size]);
 
  const drawCircle = useCallback(
    (circle: Circle, update = false) => {
      if (context.current) {
        const { x, y, size, alpha } = circle;
 
        context.current.shadowBlur = size * 3;
        context.current.shadowColor = `rgba(${rgb.join(", ")}, ${alpha})`;
 
        context.current.beginPath();
        context.current.arc(x, y, size, 0, 2 * Math.PI);
        context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`;
        context.current.fill();
 
        if (!update) {
          circles.current.push(circle);
        }
      }
    },
    [rgb]
  );
 
  const clearContext = useCallback(() => {
    if (context.current) {
      context.current.clearRect(
        0,
        0,
        canvasSize.current.w,
        canvasSize.current.h
      );
    }
  }, []);
 
  const drawParticles = useCallback(() => {
    clearContext();
    for (let i = 0; i < quantity; i++) {
      const circle = circleParams();
      drawCircle(circle);
    }
  }, [circleParams, clearContext, drawCircle, quantity]);
 
  const animate = useCallback(() => {
    clearContext();
    circles.current.forEach((circle: Circle, i: number) => {
      const currentTime = Date.now();
      const elapsedTime = currentTime - circle.fadeInStartTime;
 
      const fadeCycle = Math.sin(elapsedTime * circle.fadeSpeed);
      circle.alpha = 0.3 + 0.3 * fadeCycle;
 
      circle.x += circle.dx;
      circle.y += circle.dy - speed;
 
      drawCircle(circle, true);
 
      if (
        circle.x < -circle.size ||
        circle.x > canvasSize.current.w + circle.size ||
        circle.y < -circle.size
      ) {
        circles.current.splice(i, 1);
        const newCircle = circleParams();
        newCircle.y = canvasSize.current.h + newCircle.size;
        drawCircle(newCircle);
      }
    });
    window.requestAnimationFrame(animate);
  }, [circleParams, clearContext, drawCircle, speed]);
 
  useEffect(() => {
    if (canvasRef.current) {
      context.current = canvasRef.current.getContext("2d");
    }
 
    const initCanvas = () => {
      resizeCanvas();
      drawParticles();
    };
 
    initCanvas();
 
    animate();
    window.addEventListener("resize", initCanvas);
 
    return () => {
      window.removeEventListener("resize", initCanvas);
    };
  }, [animate, drawParticles, resizeCanvas]);
 
  return (
    <div className="relative w-full h-full overflow-hidden">
      <canvas
        ref={canvasRef}
        className="absolute top-0 left-0 w-full h-full pointer-events-none z-0 transition-opacity duration-500 ease-in-out opacity-60"
      />
      <div className="flex items-center justify-center w-full h-full relative z-10">
        {children}
      </div>
    </div>
  );
};

    Update the import paths to match your project setup.

    Props

    PropTypeDefaultDescription
    quantitynumber200The number of particles to render.
    sizenumber0.01The base size of each particle.
    colorstring"#000000"The color of particles in light mode (hex format).
    darkModeColorstring"#ffffff"The color of particles in dark mode (hex format).
    speednumber0.1The speed at which particles move upwards.
    childrenReact.ReactNode-Optional child elements to be wrapped by the Particles component.