Particle network
Dots drift across a canvas and draw lines to their neighbours. Particles are pushed gently by the cursor.
JavaScriptCanvasAdvanced
<canvas id="c" aria-hidden="true"></canvas>
<h1>Connected</h1>
body{overflow:hidden;background:#140807}
canvas{position:fixed;inset:0;width:100%;height:100%}
h1{position:relative;margin:0;font-size:clamp(2rem,8vw,3.6rem);letter-spacing:-.03em;pointer-events:none}
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let w, h, dpr, pts = [];
const mouse = { x: -999, y: -999 };
function resize() {
dpr = Math.min(devicePixelRatio || 1, 2);
w = innerWidth; h = innerHeight;
canvas.width = w * dpr; canvas.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const count = Math.round((w * h) / 9000); // density scales with screen area
pts = Array.from({ length: count }, () => ({
x: Math.random() * w, y: Math.random() * h,
vx: (Math.random() - .5) * .5, vy: (Math.random() - .5) * .5
}));
}
function frame() {
ctx.clearRect(0, 0, w, h);
for (const p of pts) {
const dx = p.x - mouse.x, dy = p.y - mouse.y, d = Math.hypot(dx, dy);
if (d < 110) { p.x += dx / d * 1.5; p.y += dy / d * 1.5; } // push away from cursor
p.x += p.vx; p.y += p.vy;
if (p.x < 0 || p.x > w) p.vx *= -1;
if (p.y < 0 || p.y > h) p.vy *= -1;
ctx.fillStyle = '#f08a5d';
ctx.beginPath(); ctx.arc(p.x, p.y, 1.8, 0, 7); ctx.fill();
}
for (let i = 0; i < pts.length; i++) {
for (let j = i + 1; j < pts.length; j++) {
const d = Math.hypot(pts[i].x - pts[j].x, pts[i].y - pts[j].y);
if (d < 110) {
ctx.strokeStyle = `rgba(240,138,93,${(1 - d / 110) * .35})`;
ctx.beginPath(); ctx.moveTo(pts[i].x, pts[i].y); ctx.lineTo(pts[j].x, pts[j].y); ctx.stroke();
}
}
}
requestAnimationFrame(frame);
}
addEventListener('resize', resize);
addEventListener('pointermove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });
resize();
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) frame();
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Particle network</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{display:grid;place-items:center;background:#140807;color:#fffffc;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
body{overflow:hidden;background:#140807}
canvas{position:fixed;inset:0;width:100%;height:100%}
h1{position:relative;margin:0;font-size:clamp(2rem,8vw,3.6rem);letter-spacing:-.03em;pointer-events:none}
</style></head>
<body>
<canvas id="c" aria-hidden="true"></canvas>
<h1>Connected</h1>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let w, h, dpr, pts = [];
const mouse = { x: -999, y: -999 };
function resize() {
dpr = Math.min(devicePixelRatio || 1, 2);
w = innerWidth; h = innerHeight;
canvas.width = w * dpr; canvas.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const count = Math.round((w * h) / 9000); // density scales with screen area
pts = Array.from({ length: count }, () => ({
x: Math.random() * w, y: Math.random() * h,
vx: (Math.random() - .5) * .5, vy: (Math.random() - .5) * .5
}));
}
function frame() {
ctx.clearRect(0, 0, w, h);
for (const p of pts) {
const dx = p.x - mouse.x, dy = p.y - mouse.y, d = Math.hypot(dx, dy);
if (d < 110) { p.x += dx / d * 1.5; p.y += dy / d * 1.5; } // push away from cursor
p.x += p.vx; p.y += p.vy;
if (p.x < 0 || p.x > w) p.vx *= -1;
if (p.y < 0 || p.y > h) p.vy *= -1;
ctx.fillStyle = '#f08a5d';
ctx.beginPath(); ctx.arc(p.x, p.y, 1.8, 0, 7); ctx.fill();
}
for (let i = 0; i < pts.length; i++) {
for (let j = i + 1; j < pts.length; j++) {
const d = Math.hypot(pts[i].x - pts[j].x, pts[i].y - pts[j].y);
if (d < 110) {
ctx.strokeStyle = `rgba(240,138,93,${(1 - d / 110) * .35})`;
ctx.beginPath(); ctx.moveTo(pts[i].x, pts[i].y); ctx.lineTo(pts[j].x, pts[j].y); ctx.stroke();
}
}
}
requestAnimationFrame(frame);
}
addEventListener('resize', resize);
addEventListener('pointermove', e => { mouse.x = e.clientX; mouse.y = e.clientY; });
resize();
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) frame();
</script>
</body></html>
Good for
Tech, data and AI hero sections. One canvas, no libraries.
Watch out for
Scale particle count with screen area, cap devicePixelRatio, and stop the loop when the tab is hidden or the canvas is off screen.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.