A Lightweight Way to Build Particle Trail Effects

Particle animations can be visually striking, but they often seem to require heavy 3D libraries. For a simple comet-like trail, a lightweight JavaScript animation library like anime.js is enough. With basic CSS and JS knowledge, you can create a glittering trail of particles — useful for hiding loading times or adding visual flair.

Setting Up the Library and Markup

Download anime.js or anime.min.js from the official GitHub repository (look in the lib/ folder) and include it in your page.

<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>Anime.js Particles</title>
  <!--or use anime.min.js-->
  <script src="anime.js"></script>
  <link rel="stylesheet" href="style.css">
</head>
<body>
<div class="anime-container">
</div>
<script src="script.js"></script>
</body>
</html>

The CSS handles the page background and particle styling. The key is positioning particles with absolute so you can control their placement via left and top.

body {
 background-color: hsl(30, 3%, 14%);
}
.anime-container {
  position: relative;
}
 
.anime-container .dot{
  position: absolute;
  /*draw particles as circles:*/
  border-radius: 50%;
  background-color: hsl(60, 100%, 80%);
}

Generating the Particle Field

All particles are created upfront, before any animation begins. In this example, the particles follow an Archimedean spiral. Each particle's left and top is derived from its angle on the spiral. The l parameter controls the number of angles (spiral length), and a controls its density.

x=a*angle*cos(angle)
y=a*angle*sin⁡(angle)

The result is a single particle per spiral position. To create a bushy trail, each position needs multiple particles with slightly offset coordinates. anime.js has a helper function for precisely this kind of random variation:

anime.random(minValue, maxValue);

Particle size also varies randomly:

for (var i = 0; i <= l; i += 1) {
  var angle = 0.1 * i;
  //shift particles to the center of the window 
  //by adding half of the screen width and screen height
  var x = (a*angle) * Math.cos(angle) + window.innerWidth / 2;
  var y = (a*angle) * Math.sin(angle) + window.innerHeight / 2;
  var n = 15;
  
  //create n particles for each angle
  for (var j = 0; j < n; j++) {
    var dot = document.createElement("div");
    dot.classList.add("dot");
    container.appendChild(dot);
    var size = anime.random(5, 10); 
    dot.style.width = size + "px";
    dot.style.height = size + "px";
    dot.style.left = x + anime.random(-15, 15) + "px";
    dot.style.top = y + anime.random(-15, 15) + "px";
    dot.style.backgroundColor = "hsl(60, 100%, 80%)";
  }
}

Before animating, hide all particles so they only appear when the animated sequence begins:

dot.style.opacity = "0";

Animating the Trail

The core animation config is simple: it loops infinitely, uses linear easing, and targets every element with the class dot.

anime({
  loop: true,
  easing: "linear",
  targets: document.querySelectorAll(".dot"),
});

Opacity: The Spiral Trick

First, make particles visible quickly — within 50ms:

anime({
  loop: true,
  easing: "linear",
  targets: document.querySelectorAll(".dot"),
  opacity: { value: 1, duration: 50}
});

The illusion of spiral motion comes from a staggered delay. Particles at the center of the spiral appear first; those further out appear later. anime.js's stagger function does this elegantly:

opacity: { value: 1, duration: 50, delay: anime.stagger(2) }

To look like a flying trail, particles should begin fading out as soon as they appear. Use property keyframes for this:

opacity: [
    { value: 1, duration: 50, delay: anime.stagger(2) },
    { value: 0, duration: 1200}
  ],

Size and Individual Drift

For a comet-like shape, particles at the front should be larger. Shrink them to 2px over 500ms, using the same stagger delay so each shrinks only after becoming visible:

width: { value: 2, duration: 500, delay: anime.stagger(2) },
height: { value: 2, duration: 500, delay: anime.stagger(2) },

The final touch is individual particle movement. Each dot needs its own translateX and translateY value. Function-based parameters are essential here — if you pass static random values, all particles would shift identically. With functions, values are computed per target:

translateX: {
    value: function() {
      return anime.random(-30, 30);
    },
    duration: 1500,
    delay: anime.stagger(2)
  },

translateY: {
    value: function() {
      return anime.random(-30, 30);
    },
    duration: 1500,
    delay: anime.stagger(2)
  }

Again, apply the same stagger delay so the drifting starts only after the particle appears.

Refining the Fade-Out

With function-based parameters in hand, the opacity timeline can also be varied per particle. Instead of a fixed decay phase, give each particle an individually randomized delay before it starts fading:

opacity: [
    { value: 1, duration: 50, delay: anime.stagger(2) },
    { value: 0, duration: function(){return anime.random(500,1500);}}
],

That small change makes the visual rhythm less uniform and noticeably more sophisticated.

Final Result and Next Steps

The completed animation is available as a working demo below. Tweaking any of the numeric parameters — spiral length, density, stagger intervals, or movement magnitude — will produce a different character of trail.

See the Pen [anime js particles](https://codepen.io/smashingmag/pen/yLNWqRP) by Anna Prenzel.
See the Pen anime js particles by Anna Prenzel.

More ambitious examples and community work with anime.js are worth exploring on CodePen.