Particle Network Background with Canvas (No Library)
Video: Particle Network Background with Canvas (No Library) by CelesteAI
The animated “connecting dots” background — points drifting around a hero section, lines snapping between the ones that get close — is one of the most-requested front-end effects. People reach for libraries like particles.js to get it. You don’t need one. It’s about sixty lines of plain Canvas 2D.
This build uses TypeScript and Vite, scaffolded from the command line and run in the browser. The only dependency is Vite itself — there’s no graphics library at all.
Set up the project
npm create vite@latest particle-network -- --template vanilla-ts
cd particle-network
npm install
That’s it — no npm install of any graphics library, because Canvas is built into the browser. Trim index.html to a full-screen canvas:
<canvas id="bg"></canvas>
<script type="module" src="/src/main.ts"></script>
html, body { margin: 0; height: 100%; background: #070b18; }
#bg { position: fixed; inset: 0; display: block; }
The canvas and the particles
Grab the canvas and its 2D context, and decide on a few constants. Each particle is just a position and a velocity.
const canvas = document.getElementById("bg") as HTMLCanvasElement;
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
const COUNT = 110;
const MAX_DIST = 150; // connect points closer than this
const SPEED = 0.5;
interface Particle { x: number; y: number; vx: number; vy: number; }
const particles: Particle[] = [];
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
function init() {
particles.length = 0;
for (let i = 0; i < COUNT; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * SPEED * 2,
vy: (Math.random() - 0.5) * SPEED * 2,
});
}
}
The frame loop
Every frame: clear the canvas, move each particle (bouncing off the edges), then the interesting part — check every pair of particles and draw a line between any two that are close, fading the line with distance. That fade is what turns a mess of lines into a living network. Finally, draw the dots on top and ask for the next frame.
function frame() {
ctx.fillStyle = "#070b18";
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const p of particles) {
p.x += p.vx;
p.y += p.vy;
if (p.x < 0 || p.x > canvas.width) p.vx *= -1;
if (p.y < 0 || p.y > canvas.height) p.vy *= -1;
}
for (let i = 0; i < COUNT; i++) {
for (let j = i + 1; j < COUNT; j++) {
const dist = Math.hypot(particles[i].x - particles[j].x, particles[i].y - particles[j].y);
if (dist < MAX_DIST) {
const alpha = (1 - dist / MAX_DIST) * 0.55;
ctx.strokeStyle = "rgba(94, 234, 212, " + alpha + ")";
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
}
}
}
ctx.fillStyle = "#a5f3ec";
for (const p of particles) {
ctx.beginPath();
ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(frame);
}
window.addEventListener("resize", () => { resize(); init(); });
resize();
init();
frame();
The pair check is O(n²) — fine for ~100–150 particles. If you push the count into the thousands, switch to a spatial grid so you only compare nearby points.
Run it
npm run dev
Open the URL Vite prints and the network drifts and connects on its own. Drop the canvas behind your content (a low z-index, position: fixed) and it’s a background.
Recap
- Canvas 2D needs no library.
getContext("2d"), then you draw every frame yourself. - The network is just a pairwise distance check — connect points closer than a threshold.
- Fade the line with distance. Bright when close, faint when far — that’s what makes it read as a living web instead of a tangle of lines.
Want the deeper dive?
The full TypeScript project is on GitHub — clone it, npm install, npm run dev, and tune the count, distance, and colors.