← Particles

Fireworks

Rockets rise from the bottom of a section and burst into showers of sparks that droop and fade. Click anywhere inside to launch one at that spot.

Click anywhere

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-fireworks to a section with a dark background.

Options

Add any of these attributes to the same element.

data-rateTime between launches. Default 900 ms. 0 launches only on click.
data-colorsComma-separated colours.
data-sparksSparks per burst. Default 70.

Drawn on a canvas behind your content and paused when off screen. From code, animacious.fireworks(section, x, y) bursts one at a point.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Fireworks: rockets rise from the bottom of a section and burst into
    // showers of sparks that droop and fade. Click inside to launch one at that
    // spot. Drawn on a canvas behind your content, and paused when off screen.
    // Usage: <section data-fireworks style="background:#0b0b1a">…</section>
    // Optional: data-rate (ms between launches, default 900; 0 = only on
    // click), data-colors (comma-separated), data-sparks (per burst, default 70).
    // Script use: animacious.fireworks(element, x, y) bursts one at a point
    // (x and y in px from the element's top-left; centre if left out).
    var DEFAULTS = { rate: 900, sparks: 70, colors: ['#a855f7', '#ec4899', '#facc15', '#38bdf8', '#4ade80', '#ffffff'] };
    var GRAVITY = 0.0008; // px per ms² for a 1000px-tall section, scaled to its height

    var css =
      '[data-fireworks]{position:relative;isolation:isolate;overflow:hidden}' +
      '.fwx-canvas{position:absolute;inset:0;width:100%;height:100%;z-index:-1;pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function build(el) {
      var d = el.dataset;
      var canvas = document.createElement('canvas');
      canvas.className = 'fwx-canvas';
      canvas.setAttribute('aria-hidden', 'true');
      el.insertBefore(canvas, el.firstChild);
      var ctx = canvas.getContext('2d');
      var colors = d.colors ? d.colors.split(',').map(function (c) { return c.trim(); }) : DEFAULTS.colors;
      var sparksPer = +d.sparks || DEFAULTS.sparks;
      var rate = d.rate !== undefined ? +d.rate : DEFAULTS.rate;
      var w = 0, h = 0, dpr = 1, unit = 1;
      var rockets = [], sparks = [], sinceLaunch = rate;

      function resize() {
        dpr = Math.min(2, window.devicePixelRatio || 1);
        w = el.clientWidth;
        h = el.clientHeight;
        unit = Math.max(0.35, h / 1000);
        canvas.width = w * dpr;
        canvas.height = h * dpr;
      }
      resize();
      if (window.ResizeObserver) new ResizeObserver(resize).observe(el);

      function pick() { return colors[(Math.random() * colors.length) | 0]; }

      function launch(tx, ty) {
        tx = tx === undefined ? w * (0.2 + Math.random() * 0.6) : tx;
        ty = ty === undefined ? h * (0.15 + Math.random() * 0.3) : ty;
        var x = tx + (Math.random() - 0.5) * w * 0.1;
        // Speed it needs to just reach the target height under gravity.
        var g = GRAVITY * unit;
        var vy = -Math.sqrt(2 * g * (h - ty));
        var t = -vy / g;
        rockets.push({ x: x, y: h, vx: (tx - x) / t, vy: vy, color: pick() });
      }

      function burst(x, y, color) {
        var n = sparksPer;
        for (var i = 0; i < n; i++) {
          var a = (i / n) * Math.PI * 2 + Math.random() * 0.2;
          var s = (0.05 + Math.random() * 0.12) * Math.max(0.6, unit * 1.6);
          sparks.push({
            x: x, y: y, px: x, py: y, vx: Math.cos(a) * s, vy: Math.sin(a) * s,
            life: 0, max: 900 + Math.random() * 700,
            color: Math.random() < 0.8 ? color : pick()
          });
        }
      }

      function step(dt) {
        var g = GRAVITY * unit;
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, w, h);
        ctx.lineCap = 'round';
        if (rate > 0) {
          sinceLaunch += dt;
          if (sinceLaunch >= rate) { sinceLaunch = Math.random() * rate * 0.4; launch(); }
        }
        rockets = rockets.filter(function (r) {
          var px = r.x, py = r.y;
          r.vy += g * dt;
          r.x += r.vx * dt;
          r.y += r.vy * dt;
          ctx.globalAlpha = 1;
          ctx.strokeStyle = r.color;
          ctx.lineWidth = 2;
          ctx.beginPath();
          ctx.moveTo(px, py + (py - r.y) * 3);
          ctx.lineTo(r.x, r.y);
          ctx.stroke();
          if (r.vy >= 0) { burst(r.x, r.y, r.color); return false; }
          return true;
        });
        sparks = sparks.filter(function (s) {
          s.life += dt;
          if (s.life >= s.max) return false;
          s.px = s.x; s.py = s.y;
          var drag = Math.pow(0.985, dt / 16);
          s.vx *= drag; s.vy = s.vy * drag + g * dt * 0.6;
          s.x += s.vx * dt;
          s.y += s.vy * dt;
          var k = 1 - s.life / s.max;
          // Twinkle a little as they die out.
          ctx.globalAlpha = k * (k < 0.4 ? 0.5 + Math.random() * 0.5 : 1);
          ctx.strokeStyle = s.color;
          ctx.lineWidth = 1 + 1.6 * k;
          ctx.beginPath();
          ctx.moveTo(s.px - s.vx * dt * 2, s.py - s.vy * dt * 2);
          ctx.lineTo(s.x, s.y);
          ctx.stroke();
          return true;
        });
      }

      var last = 0;
      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = last ? Math.min(50, now - last) : 16;
          last = now;
          step(dt);
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);

      el.addEventListener('click', function (e) {
        if (e.target.closest('a,button,input,select,textarea,label')) return;
        var r = el.getBoundingClientRect();
        launch(e.clientX - r.left, e.clientY - r.top);
      });
      el._fwx = {
        burst: function (x, y) { burst(x === undefined ? w / 2 : x, y === undefined ? h / 2 : y, pick()); }
      };
    }

    function init() {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      document.querySelectorAll('[data-fireworks]').forEach(function (el) {
        if (el._fwx) return;
        build(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.fireworks = function (el, x, y) { if (el && el._fwx) el._fwx.burst(x, y); };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<section data-fireworks style="background: #0b0b1a; color: #fff">
  <h1>Happy launch day</h1>
</section>