Glowing blue and violet particles connected across a dark background
Blog

Build a JavaScript Particle Background with HTML Canvas

Create a lightweight animated background without adding an animation library. This tutorial walks through a responsive canvas particle effect that connects nearby points, responds to pointer movement, and includes speed and pause controls.

Quick answer: Add a canvas behind your hero content, size it to the section, draw a capped number of moving particles with the Canvas 2D API, and update them with requestAnimationFrame(). Keep text and controls in normal HTML so the page remains usable when animation is paused.

Try the live demoDownload the source ZIP

What you will build

The demo draws small particles and faint connecting lines on a canvas. Moving your pointer gently influences nearby points. Visitors can choose a slower or faster animation and pause it at any time. The project uses plain HTML, CSS, and JavaScript, with no external animation dependency.

Project files

The download includes index.html, styles.css, and script.js. Keep the three files in the same folder, then open index.html in a modern browser. The code examples below match those files.

1. Add the canvas and accessible controls

Use a canvas for decoration, and keep the headline, explanation, speed selector, and pause button as regular HTML. The canvas has aria-hidden="true" because it conveys no information that is not already available in text.

2. Layer and style the animation

Give the hero section position: relative and isolation: isolate. Stretch the canvas across the section at a lower stacking level, then place the content and controls above it. The downloadable stylesheet also adjusts spacing and control layout on smaller screens.

3. Animate particles with JavaScript

The script measures the demo area, creates a limited set of particles, and draws connections only when two points are close enough. requestAnimationFrame() schedules each frame efficiently. The canvas scales for high-density screens while capping the pixel ratio to avoid unnecessary rendering work.

The code also checks prefers-reduced-motion. When a visitor has requested reduced motion, the animation starts paused. The pause control remains available so each visitor can choose whether to play it.

4. Customize the background

  • Adjust connectionDistance to change how far apart connected points can be.
  • Lower the particle limit in makeParticles() to reduce rendering work on phones.
  • Change the canvas and line colors in drawFrame() to fit your site palette.
  • Change the speed values in the HTML select options.

Use this effect in Elementor

The live example on this page uses an Elementor HTML widget for the interactive demo. To add the effect to another Elementor page, place the markup in an HTML widget, add the CSS through your theme or page custom CSS, and load JavaScript through a trusted site-level script method. A separate enqueued JavaScript file is a good option if WordPress or a security plugin removes inline scripts. Avoid placing essential content inside the canvas.

Accessibility and performance checklist

  • Keep the headline and links outside the canvas and preserve strong text contrast.
  • Provide a pause button and honor the reduced-motion preference.
  • Use a modest particle cap; drawing every pair of points becomes expensive as the count grows.
  • Test keyboard focus, touch devices, and narrow mobile layouts.

Troubleshooting

  • No animation: Confirm the canvas selector matches the HTML ID and that the JavaScript runs after the markup loads.
  • Blurry canvas: Set the drawing buffer to the element dimensions multiplied by the device pixel ratio, then scale the drawing context.
  • Text hidden behind graphics: Keep the canvas at a lower stacking order and set the content wrapper to a higher one.
  • Slow page: Reduce the particle cap and connection distance, then test on a phone.

Related tutorials

Conclusion

A canvas particle background can add subtle motion to a hero section while keeping the implementation dependency-free. Try the interactive demo, download the complete files, and tune the colors and particle count to fit your design.

Reviewed and updated October 5, 2026.

HTML Canvas + Vanilla JavaScript

Ideas connect here.

Move your pointer over the canvas and try the speed and pause controls.

The canvas is decorative. Controls support keyboard focus and reduced-motion preferences.

JavaScript: responsive canvas particle animation
(() => {
  "use strict";
  const stage = document.querySelector("#network-stage"), canvas = document.querySelector("#network-canvas");
  const speedControl = document.querySelector("#motion-speed"), toggleButton = document.querySelector("#motion-toggle");
  const context = canvas?.getContext("2d");
  if (!stage || !canvas || !context || !speedControl || !toggleButton) return;
  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"), pointer = { x: null, y: null };
  let particles = [], width = 0, height = 0, frameId = 0, speed = Number(speedControl.value) || 1;
  let paused = reduceMotion.matches; const connectionDistance = 135;
  function makeParticles() {
    const amount = Math.min(100, Math.max(28, Math.round((width * height) / 10500)));
    particles = Array.from({ length: amount }, () => ({ x: Math.random() * width, y: Math.random() * height, vx: (Math.random() - 0.5) * 0.42, vy: (Math.random() - 0.5) * 0.42, radius: Math.random() * 1.8 + 1 }));
  }
  function drawFrame(move) {
    context.clearRect(0, 0, width, height); const limit = connectionDistance * connectionDistance;
    for (let i = 0; i < particles.length; i += 1) {
      const p = particles[i];
      if (move) {
        p.x += p.vx * speed; p.y += p.vy * speed;
        if (p.x < 0 || p.x > width) p.vx *= -1; if (p.y < 0 || p.y > height) p.vy *= -1;
        if (pointer.x !== null) { const dx = p.x - pointer.x, dy = p.y - pointer.y, d = Math.hypot(dx, dy); if (d > 0 && d < 110) { const f = ((110 - d) / 110) * 0.32; p.x += dx / d * f; p.y += dy / d * f; } }
      }
      for (let j = i + 1; j < particles.length; j += 1) {
        const q = particles[j], dx = p.x - q.x, dy = p.y - q.y, d2 = dx * dx + dy * dy;
        if (d2 < limit) { const alpha = (1 - Math.sqrt(d2) / connectionDistance) * 0.42; context.beginPath(); context.strokeStyle = "rgba(103, 190, 255, " + alpha + ")"; context.lineWidth = 1; context.moveTo(p.x, p.y); context.lineTo(q.x, q.y); context.stroke(); }
      }
      context.beginPath(); context.fillStyle = "rgba(165, 216, 255, 0.9)"; context.arc(p.x, p.y, p.radius, 0, Math.PI * 2); context.fill();
    }
  }
  function animate() { drawFrame(true); if (!paused) frameId = requestAnimationFrame(animate); }
  function resizeCanvas() { const b = stage.getBoundingClientRect(); width = Math.max(1, b.width); height = Math.max(1, b.height); const dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = Math.round(width * dpr); canvas.height = Math.round(height * dpr); context.setTransform(dpr, 0, 0, dpr, 0, 0); makeParticles(); drawFrame(false); }
  function setPaused(next) { paused = next; toggleButton.textContent = paused ? "Play animation" : "Pause animation"; toggleButton.setAttribute("aria-pressed", String(paused)); cancelAnimationFrame(frameId); drawFrame(false); if (!paused) frameId = requestAnimationFrame(animate); }
  speedControl.addEventListener("change", () => { speed = Number(speedControl.value) || 1; });
  toggleButton.addEventListener("click", () => setPaused(!paused));
  stage.addEventListener("pointermove", (event) => { const b = stage.getBoundingClientRect(); pointer.x = event.clientX - b.left; pointer.y = event.clientY - b.top; });
  stage.addEventListener("pointerleave", () => { pointer.x = null; pointer.y = null; });
  window.addEventListener("resize", resizeCanvas, { passive: true });
  reduceMotion.addEventListener?.("change", (event) => setPaused(event.matches));
  resizeCanvas(); setPaused(paused);
})();
CSS: responsive canvas and controls
:root {
  color-scheme: dark;
  font-family: system-ui, sans-serif;
  background: #080d1b;
  color: #f8fafc;
}

* { box-sizing: border-box; }

body {
  min-width: 320px;
  min-height: 100vh;
  margin: 0;
  padding: clamp(20px, 5vw, 64px);
  background: radial-gradient(circle at 15% 10%, #172245 0, #080d1b 44rem);
}

.network-stage {
  position: relative;
  display: grid;
  min-height: 520px;
  overflow: hidden;
  align-items: center;
  border: 1px solid rgb(148 163 184 / 24%);
  border-radius: 24px;
  background: linear-gradient(135deg, #101a35, #080d1b 68%);
  isolation: isolate;
}

.network-canvas { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; }
.stage-copy { position: relative; z-index: 1; width: min(620px, 100%); padding: clamp(28px, 7vw, 80px); }
.stage-controls { position: absolute; z-index: 1; right: 20px; bottom: 20px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; padding: 12px; border: 1px solid rgb(148 163 184 / 25%); border-radius: 14px; background: rgb(8 13 27 / 82%); }
.stage-controls select, .motion-toggle { min-height: 42px; border: 1px solid #64748b; border-radius: 9px; padding: 0 12px; background: #111b32; color: #fff; font: inherit; }
.motion-toggle { cursor: pointer; border-color: #60a5fa; background: #1d4ed8; font-weight: 700; }
.stage-controls :focus-visible { outline: 3px solid #fbbf24; outline-offset: 3px; }

@media (max-width: 640px) {
  body { padding: 22px 14px; }
  .network-stage { min-height: 570px; align-items: start; }
  .stage-copy { padding: 36px 24px 160px; }
  .stage-controls { right: 14px; bottom: 14px; left: 14px; justify-content: space-between; }
}
HTML: complete demo document
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="A responsive animated particle network background built with HTML Canvas and vanilla JavaScript.">
  <title>JavaScript Canvas Particle Background Demo</title>
  <link rel="stylesheet" href="styles.css">
  <script src="script.js" defer></script>
</head>
<body>
  <main class="demo-shell">
    <header class="demo-header">
      <p class="eyebrow">HTML Canvas + Vanilla JavaScript</p>
      <h1>Animated particle background</h1>
      <p>A lightweight network animation that responds to pointer movement.</p>
    </header>
    <section class="network-stage" id="network-stage" aria-label="Interactive particle network background demo">
      <canvas class="network-canvas" id="network-canvas" aria-hidden="true"></canvas>
      <div class="stage-copy">
        <p class="stage-label">Ideas connect here</p>
        <h2>Build motion with a few lines of JavaScript.</h2>
        <p>Move your pointer across the canvas and adjust the animation speed.</p>
      </div>
      <div class="stage-controls">
        <label for="motion-speed">Animation speed</label>
        <select id="motion-speed">
          <option value="0.55">Slow</option>
          <option value="1" selected>Normal</option>
          <option value="1.45">Fast</option>
        </select>
        <button class="motion-toggle" id="motion-toggle" type="button" aria-pressed="false">Pause animation</button>
      </div>
    </section>
    <p class="demo-note">The canvas is decorative. The controls work with a keyboard and respect reduced-motion preferences.</p>
  </main>
</body>
</html>