Creative Visual Effects

Particle Trail Effect

Colorful glowing particles that flow along mouse or touch trails.

HTMLCanvasJavaScriptParticles
With JavaScript enabled, you can edit, run, and save this example. The fixed source is as follows:
<!doctype html>
<html lang="en" dir="ltr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Particle Trail Effect</title>
  <style>
    * { box-sizing: border-box; }
    :root { color-scheme: dark; }
    body { min-height: 100vh; margin: 0; overflow: hidden; color: #f3f5ff; font-family: "Avenir Next", "Microsoft YaHei", sans-serif; background: #05050d; }
    canvas { position: fixed; inset: 0; width: 100%; height: 100%; touch-action: none; cursor: crosshair; }
    .noise { position: fixed; inset: 0; pointer-events: none; opacity: .18; background-image: radial-gradient(rgba(255,255,255,.22) .7px, transparent .7px); background-size: 7px 7px; mask-image: linear-gradient(to bottom, black, transparent 85%); }
    header { position: fixed; z-index: 2; top: clamp(28px, 8vh, 78px); left: clamp(24px, 7vw, 80px); max-width: 620px; pointer-events: none; }
    .eyebrow { color: #71f5d0; font-size: 10px; font-weight: 900; letter-spacing: .28em; }
    h1 { margin: 12px 0 14px; font: 500 clamp(46px, 10vw, 96px)/.9 Georgia, "STSong", serif; letter-spacing: -.05em; }
    p { max-width: 470px; margin: 0; color: #848aa3; line-height: 1.75; }
    .palette { position: fixed; z-index: 3; right: 24px; bottom: 24px; display: flex; align-items: center; gap: 10px; padding: 9px 12px; border: 1px solid #24263a; border-radius: 999px; color: #8e94ad; font-size: 11px; background: rgba(10,10,22,.78); backdrop-filter: blur(10px); }
    button { width: 30px; height: 30px; border: 2px solid #fff; border-radius: 50%; background: linear-gradient(135deg, #71f5d0, #6a78ff, #ff6fae); cursor: pointer; box-shadow: 0 0 15px rgba(113,245,208,.35); }
    button:focus-visible { outline: 3px solid #fff; outline-offset: 3px; }
  </style>
</head>
<body>
  <canvas id="particles" aria-label="Generate a Particle Trail by Moving the Pointer"></canvas><div class="noise" aria-hidden="true"></div>
  <header><span class="eyebrow">INTERACTIVE LIGHT FIELD</span><h1>Touch a Beam of Light</h1><p>Move the mouse or finger, and colorful particles will grow along the trail, drift, and slowly fade away.</p></header>
  <div class="palette"><span>Tap to switch tone</span><button id="palette" type="button" aria-label="Toggle particle color tone"></button></div>
  <script>
    const canvas = document.querySelector('#particles');
    const context = canvas.getContext('2d');
    const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
    const palettes = [[160, 250], [280, 350], [25, 90]];
    let paletteIndex = 0, width = 0, height = 0, particles = [], pointer = { x: 0, y: 0, active: false };

    function resize() {
      const ratio = Math.min(devicePixelRatio || 1, 2);
      width = innerWidth; height = innerHeight;
      canvas.width = Math.round(width * ratio); canvas.height = Math.round(height * ratio);
      context.setTransform(ratio, 0, 0, ratio, 0, 0);
    }
    function emit(x, y, amount = 7) {
      const range = palettes[paletteIndex];
      for (let index = 0; index < (reduceMotion ? Math.min(amount, 2) : amount); index += 1) {
        const angle = Math.random() * Math.PI * 2, speed = .3 + Math.random() * 1.8;
        particles.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, size: 1.5 + Math.random() * 4, life: 1, decay: .012 + Math.random() * .018, hue: range[0] + Math.random() * (range[1] - range[0]) });
      }
      if (particles.length > 650) particles.splice(0, particles.length - 650);
    }
    function movePointer(event) {
      const rect = canvas.getBoundingClientRect(); pointer = { x: event.clientX - rect.left, y: event.clientY - rect.top, active: true }; emit(pointer.x, pointer.y);
    }
    function frame() {
      context.fillStyle = 'rgba(5, 5, 13, .16)'; context.fillRect(0, 0, width, height);
      context.globalCompositeOperation = 'lighter';
      particles = particles.filter(particle => {
        particle.x += particle.vx; particle.y += particle.vy; particle.vx *= .985; particle.vy = particle.vy * .985 - .008; particle.life -= particle.decay;
        if (particle.life <= 0) return false;
        context.globalAlpha = particle.life;
        context.fillStyle = `hsl(${particle.hue} 90% 65%)`;
        context.beginPath(); context.arc(particle.x, particle.y, particle.size * particle.life, 0, Math.PI * 2); context.fill(); return true;
      });
      context.globalAlpha = 1; context.globalCompositeOperation = 'source-over'; requestAnimationFrame(frame);
    }
    canvas.addEventListener('pointermove', movePointer);
    canvas.addEventListener('pointerdown', event => { movePointer(event); emit(pointer.x, pointer.y, 28); });
    canvas.addEventListener('pointerleave', () => { pointer.active = false; });
    document.querySelector('#palette').addEventListener('click', () => { paletteIndex = (paletteIndex + 1) % palettes.length; emit(width * .75, height * .65, 40); });
    addEventListener('resize', resize); resize(); emit(width * .72, height * .64, 55); requestAnimationFrame(frame);
  </script>
</body>
</html>
Page feedbackFound an issue or have suggestions for improvement?
Feedback features need to be used in the online version

Please enable JavaScript in the online version before submitting; local features of the current tool or example are not affected.

Go to the online version for feedback