Particles Background
Floating particle system with mouse-repel interaction and connecting lines
backgroundparticlesanimationinteractive
Install dependencies
$npm install framer-motionPreview
Source Code
"use client";
import { useRef, useEffect, useCallback } from "react";
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
baseVx: number;
baseVy: number;
size: number;
}
interface ParticlesBackgroundProps {
children?: React.ReactNode;
particleCount?: number;
color?: string;
connectDistance?: number;
repelRadius?: number;
className?: string;
}
export default function ParticlesBackground({
children,
particleCount = 60,
color = "#38bdf8",
connectDistance = 120,
repelRadius = 120,
className = "",
}: ParticlesBackgroundProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const mouseRef = useRef({ x: -1000, y: -1000 });
const particlesRef = useRef<Particle[]>([]);
const animationRef = useRef<number>(0);
const initParticles = useCallback(
(width: number, height: number) => {
particlesRef.current = Array.from({ length: particleCount }, () => {
const baseVx = (Math.random() - 0.5) * 0.8;
const baseVy = (Math.random() - 0.5) * 0.8;
return {
x: Math.random() * width,
y: Math.random() * height,
vx: baseVx,
vy: baseVy,
baseVx,
baseVy,
size: Math.random() * 2 + 1.5,
};
});
},
[particleCount]
);
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let cssWidth = 0;
let cssHeight = 0;
const resize = () => {
const dpr = window.devicePixelRatio || 1;
cssWidth = container.clientWidth;
cssHeight = container.clientHeight;
canvas.width = cssWidth * dpr;
canvas.height = cssHeight * dpr;
canvas.style.width = `${cssWidth}px`;
canvas.style.height = `${cssHeight}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
initParticles(cssWidth, cssHeight);
};
resize();
window.addEventListener("resize", resize);
const handleMouseMove = (e: MouseEvent) => {
const rect = container.getBoundingClientRect();
mouseRef.current = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
};
};
const handleMouseLeave = () => {
mouseRef.current = { x: -1000, y: -1000 };
};
container.addEventListener("mousemove", handleMouseMove);
container.addEventListener("mouseleave", handleMouseLeave);
const animate = () => {
ctx.clearRect(0, 0, cssWidth, cssHeight);
const particles = particlesRef.current;
const mouse = mouseRef.current;
for (const p of particles) {
// Mouse repel
const dx = p.x - mouse.x;
const dy = p.y - mouse.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < repelRadius && dist > 0) {
const force = (repelRadius - dist) / repelRadius;
p.vx += (dx / dist) * force * 1.2;
p.vy += (dy / dist) * force * 1.2;
}
// Ease back toward base velocity
p.vx += (p.baseVx - p.vx) * 0.05;
p.vy += (p.baseVy - p.vy) * 0.05;
// Move
p.x += p.vx;
p.y += p.vy;
// Wrap edges
if (p.x < -5) p.x = cssWidth + 5;
if (p.x > cssWidth + 5) p.x = -5;
if (p.y < -5) p.y = cssHeight + 5;
if (p.y > cssHeight + 5) p.y = -5;
// Draw particle with glow
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.globalAlpha = 0.9;
ctx.fill();
// Glow
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * 3, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.globalAlpha = 0.08;
ctx.fill();
}
// Draw connecting lines
ctx.strokeStyle = color;
ctx.lineWidth = 1;
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < connectDistance) {
ctx.globalAlpha = (1 - dist / connectDistance) * 0.6;
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
}
}
}
ctx.globalAlpha = 1;
animationRef.current = requestAnimationFrame(animate);
};
animate();
return () => {
cancelAnimationFrame(animationRef.current);
window.removeEventListener("resize", resize);
container.removeEventListener("mousemove", handleMouseMove);
container.removeEventListener("mouseleave", handleMouseLeave);
};
}, [color, connectDistance, repelRadius, initParticles]);
return (
<div ref={containerRef} className={`relative overflow-hidden bg-[#050510] ${className}`}>
<canvas ref={canvasRef} className="absolute inset-0" />
<div className="relative z-10 pointer-events-none">{children}</div>
</div>
);
}