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
Prop | Type | Default | Description |
---|---|---|---|
quantity | number | 200 | The number of particles to render. |
size | number | 0.01 | The base size of each particle. |
color | string | "#000000" | The color of particles in light mode (hex format). |
darkModeColor | string | "#ffffff" | The color of particles in dark mode (hex format). |
speed | number | 0.1 | The speed at which particles move upwards. |
children | React.ReactNode | - | Optional child elements to be wrapped by the Particles component. |