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
- Paste the snippet just before
</head>. - Add
data-countdown-launchto an empty element. Size it withfont-size; the numbers are 4em tall and it fills its container’s width.
Options
Add any of these attributes to the same element.
data-from | Where the count starts. Default 3. |
data-text | The word at the end. Default Go! |
data-interval | How long each number shows. Default 900 ms. |
data-loop | Count down again every few seconds. |
data-hold | How long the word stays before looping. Default 2200 ms. |
data-color | Number colour. Default the text colour. |
data-colors | Comma-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).
<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><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>