← Numbers

Countdown Launch

3, 2, 1 zoom in one at a time, each with a pulse ring and a ring that fills around them, then the final word pops in with a flash and a burst of sparks. Use it before a reveal, a launch or the start of a game.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-countdown-launch to an empty element. Size it with font-size; the numbers are 4em tall and it fills its container’s width.

Options

Add any of these attributes to the same element.

data-fromWhere the count starts. Default 3.
data-textThe word at the end. Default Go!
data-intervalHow long each number shows. Default 900 ms.
data-loopCount down again every few seconds.
data-holdHow long the word stays before looping. Default 2200 ms.
data-colorNumber colour. Default the text colour.
data-colorsComma-separated burst colours; the first two also colour the word.

Click or tap it to count down again, or start it from a script with animacious.countdownLaunch(el).

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Countdown Launch: 3, 2, 1 zoom in one at a time, each with a pulse ring,
    // then the last word pops in with a burst of confetti sparks.
    // Usage: <div data-countdown-launch></div>
    // Optional: data-from (where the count starts, default 3), data-text (the
    // word at the end, default "Go!"), data-interval (ms per number, default
    // 900), data-loop (count down again every few seconds), data-hold (ms the
    // word stays before looping, default 2200), data-color (number colour,
    // default the text colour), data-colors (comma-separated burst colours).
    // Click or tap it to count down again.
    // Script use: animacious.countdownLaunch(el) starts it again.
    // Size it with font-size: the numbers are 4em tall.
    var DEFAULTS = { from: 3, text: 'Go!', interval: 900, hold: 2200, colors: '#6a0dad,#ff4d8d,#ffb020,#22c55e,#3b82f6' };

    var css =
      '.ctl{position:relative;display:grid;place-items:center;width:100%;height:6.4em;overflow:hidden;cursor:pointer;user-select:none;-webkit-tap-highlight-color:transparent}' +
      '.ctl>*{grid-area:1/1;pointer-events:none}' +
      '.ctl-n,.ctl-go{font-size:4em;font-weight:900;line-height:1;letter-spacing:-.04em;font-variant-numeric:tabular-nums;opacity:0;color:var(--ctl-c,currentColor)}' +
      '.ctl-go{font-size:2.6em;white-space:nowrap;background:linear-gradient(135deg,var(--ctl-a),var(--ctl-b));-webkit-background-clip:text;background-clip:text;color:transparent;padding:0 .1em}' +
      '.ctl-ring{width:3.6em;height:3.6em;border-radius:50%;border:.14em solid var(--ctl-c,currentColor);opacity:0}' +
      '.ctl-track{width:5em;height:5em;opacity:.9}' +
      '.ctl-track circle{fill:none;stroke-width:2.2;stroke-linecap:round}' +
      '.ctl-flash{width:3em;height:3em;border-radius:50%;background:radial-gradient(closest-side,var(--ctl-b),transparent);opacity:0}' +
      '.ctl-bit{position:relative;width:.42em;height:.42em;border-radius:50%;opacity:0}' +
      '.ctl-bit.ctl-streak{width:.18em;height:.8em;border-radius:.1em}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function rand(a, b) { return a + Math.random() * (b - a); }

    function setup(el) {
      var d = el.dataset;
      var C = {
        el: el, from: Math.max(1, parseInt(d.from, 10) || DEFAULTS.from), text: d.text || DEFAULTS.text,
        interval: +d.interval || DEFAULTS.interval, hold: +d.hold || DEFAULTS.hold,
        colors: (d.colors || DEFAULTS.colors).split(','), loop: d.loop !== undefined && d.loop !== 'false',
        still: reduced(), visible: false, playing: false, timers: [], run: 0
      };
      el.classList.add('ctl');
      if (d.color) el.style.setProperty('--ctl-c', d.color);
      el.style.setProperty('--ctl-a', C.colors[0]);
      el.style.setProperty('--ctl-b', C.colors[1] || C.colors[0]);
      var bits = '';
      for (var i = 0; i < 22; i++) bits += '<span class="ctl-bit' + (i % 3 === 0 ? ' ctl-streak' : '') + '" style="background:' + C.colors[i % C.colors.length] + '"></span>';
      el.innerHTML = '<svg class="ctl-track" viewBox="0 0 48 48" aria-hidden="true"><circle cx="24" cy="24" r="22" stroke="currentColor" opacity=".12"/>' +
        '<circle class="ctl-arc" cx="24" cy="24" r="22" style="stroke:var(--ctl-a)" transform="rotate(-90 24 24)" pathLength="100" stroke-dasharray="100" stroke-dashoffset="100"/></svg>' +
        '<span class="ctl-ring" aria-hidden="true"></span><span class="ctl-flash" aria-hidden="true"></span>' + bits +
        '<span class="ctl-n" aria-hidden="true"></span><span class="ctl-go" aria-hidden="true">' + C.text + '</span>';
      C.n = el.querySelector('.ctl-n');
      C.go = el.querySelector('.ctl-go');
      C.ring = el.querySelector('.ctl-ring');
      C.flash = el.querySelector('.ctl-flash');
      C.track = el.querySelector('.ctl-track');
      C.arc = el.querySelector('.ctl-arc');
      C.bits = [].slice.call(el.querySelectorAll('.ctl-bit'));
      el.setAttribute('role', 'img');
      el.setAttribute('aria-label', 'Countdown from ' + C.from + ' to ' + C.text);
      el.addEventListener('click', function () { play(C); });
      return C;
    }

    function later(C, ms, fn) {
      var run = C.run;
      C.timers.push(setTimeout(function () { if (run === C.run) fn(); }, ms));
    }

    function cancel(C) {
      C.run++;
      C.timers.forEach(clearTimeout);
      C.timers = [];
    }

    function tick(C, n) {
      C.n.textContent = n;
      anime.animate(C.n, { scale: [0.25, 1], opacity: [0, 1], duration: 380, ease: 'outBack(2)' });
      later(C, C.interval - 280, function () { anime.animate(C.n, { scale: 1.8, opacity: 0, duration: 240, ease: 'in(2)' }); });
      anime.animate(C.ring, { scale: [0.7, 1.7], opacity: [0.55, 0], duration: 800, ease: 'out(3)' });
      var step = (C.from - n + 1) / C.from;
      anime.animate(C.arc, { strokeDashoffset: 100 - step * 100, duration: C.interval * 0.8, ease: 'inOut(2)' });
    }

    function burst(C) {
      C.go.style.opacity = 1;
      anime.animate(C.go, { scale: [0, 1], rotate: [-12, 0], duration: 900, ease: 'outElastic(1, .55)' });
      anime.animate(C.track, { scale: [1, 1.5], opacity: [0.9, 0], duration: 500, ease: 'out(2)' });
      anime.animate(C.flash, { scale: [0.3, 3.2], opacity: [0.9, 0], duration: 700, ease: 'out(3)' });
      anime.animate(C.ring, { scale: [0.8, 2.6], opacity: [0.7, 0], duration: 900, ease: 'out(3)' });
      var h = C.el.clientHeight, w = C.el.clientWidth, reach = Math.min(h * 0.46, w * 0.42);
      C.bits.forEach(function (b, i) {
        var a = (i / C.bits.length) * Math.PI * 2 + rand(-0.15, 0.15), r = reach * rand(0.55, 1);
        anime.animate(b, {
          x: [0, Math.cos(a) * r], y: [0, Math.sin(a) * r + reach * 0.15], rotate: [a * 180 / Math.PI + 90, a * 180 / Math.PI + 90 + rand(-90, 90)],
          scale: [1.3, 0.3], opacity: [{ to: 1, duration: 60 }, { to: 1, duration: 500 }, { to: 0, duration: 400 }],
          duration: 960, ease: 'out(4)'
        });
      });
    }

    function play(C) {
      if (C.still) return;
      cancel(C);
      C.playing = true;
      anime.utils.set([C.n, C.go], { opacity: 0 });
      anime.utils.set(C.track, { scale: 1, opacity: 0.9 });
      anime.utils.set(C.arc, { strokeDashoffset: 100 });
      for (var k = 0; k < C.from; k++) later(C, 250 + k * C.interval, tick.bind(null, C, C.from - k));
      later(C, 250 + C.from * C.interval, function () {
        burst(C);
        C.playing = false;
        if (C.loop) later(C, C.hold, function () {
          anime.animate(C.go, { opacity: 0, scale: 0.6, duration: 350, ease: 'in(2)' });
          later(C, 450, function () { if (C.visible) play(C); else C.waiting = true; });
        });
      });
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var C = e.target._ctl;
          C.visible = e.isIntersecting;
          if (C.visible && (!C.started || C.waiting || C.playing)) { C.started = true; C.waiting = false; play(C); }
          else if (!C.visible && C.playing) { cancel(C); C.waiting = true; }
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-countdown-launch]').forEach(function (el) {
        if (el._ctl) return;
        var C = (el._ctl = setup(el));
        if (C.still) {
          C.go.style.opacity = 1;
          C.arc.setAttribute('stroke-dashoffset', 0);
          return;
        }
        io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.countdownLaunch = function (el) { if (el && el._ctl) play(el._ctl); };

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-countdown-launch></div>

<!-- from 5, ending on your own word: -->
<div data-countdown-launch data-from="5" data-text="Launch!" style="font-size: 28px"></div>