Starting Soon Screen
A holding screen for a stream. Soft colour glows drift slowly behind your heading, a little equaliser bounces beside the countdown, and a bar fills as the time runs down. At zero it swaps to “Starting now”.
Starting soon
Grab a drink, we’ll be right with you
How to use it
- Paste the snippet just before
</head>. - Add
data-starting-soon-screento a box with a height, holding your heading and any message. - Set the countdown with
data-time="5:00", or count down to a real start withdata-until.
Options
Add any of these attributes to the same element.
data-time | Countdown length, as m:ss or seconds. Default 5:00. |
data-until | A date and time to count down to instead, e.g. 2026-12-01T19:00. |
data-done | Text shown at zero. Default “Starting now”. |
data-colors | Comma-separated glow colours. Default purple, pink, blue and indigo. |
data-loop | Start the countdown again a few seconds after it ends. |
data-eq="false" | Leave out the bouncing equaliser. |
The countdown follows the real clock, so it stays right even if the tab sleeps. Give the box your own background to change the dark backdrop.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Starting Soon Screen: a holding screen for a stream. Soft colour glows
// drift slowly behind your heading, a little equaliser bounces along, and
// a countdown ticks down with a bar filling beneath it. At zero it swaps
// to a "Starting now" message.
// Usage: <div data-starting-soon-screen data-time="5:00" style="height: 400px">
// <h2>Starting soon</h2>
// <p>Grab a drink, we'll be right with you</p>
// </div>
// Optional: data-time (countdown length, "m:ss" or seconds, default
// 5:00), data-until (a date and time to count down to instead, e.g.
// "2026-12-01T19:00"), data-done (text shown at zero, default "Starting
// now"), data-colors (comma-separated glow colours, default purple, pink
// and blue), data-loop (start the countdown again a few seconds after it
// ends; handy for demos), data-eq="false" (no equaliser).
var DEFAULTS = { time: 300, done: 'Starting now', colors: '#a855f7,#ec4899,#38bdf8,#6366f1' };
var css =
':where(.sss){position:relative;background:#0b0a1a;color:#fff}' +
'.sss{overflow:hidden;isolation:isolate;display:grid;place-items:center;text-align:center}' +
'.sss-bg{position:absolute;inset:0;z-index:-1;pointer-events:none;overflow:hidden}' +
'.sss-orb{position:absolute;left:0;top:0;border-radius:50%;opacity:.7;filter:blur(30px);will-change:transform}' +
'.sss-bg::after{content:"";position:absolute;inset:0;background-image:radial-gradient(rgba(255,255,255,.12) 1px,transparent 1.2px);background-size:22px 22px;-webkit-mask-image:radial-gradient(closest-side,#000,transparent);mask-image:radial-gradient(closest-side,#000,transparent)}' +
'.sss-body{display:flex;flex-direction:column;align-items:center;padding:1em}' +
'.sss-body>h1,.sss-body>h2,.sss-body>h3{margin:0;font-size:2.1em;font-weight:800;letter-spacing:-0.03em;line-height:1.05}' +
'.sss-body>p{margin:0.5em 0 0;opacity:.75}' +
'.sss-clock{display:flex;align-items:center;gap:0.5em;margin-top:0.9em}' +
'.sss-time{font-size:1.9em;font-weight:800;font-variant-numeric:tabular-nums;letter-spacing:0.02em;line-height:1}' +
'.sss-time span{display:inline-block}' +
'.sss-eq{display:flex;align-items:flex-end;gap:0.14em;height:1.3em}' +
'.sss-eq i{display:block;width:0.22em;height:100%;border-radius:0.1em;background:currentColor;opacity:.8;transform-origin:50% 100%;transform:scaleY(.3)}' +
'.sss-track{width:min(14em,70%);height:0.28em;margin-top:0.9em;border-radius:1em;background:rgba(255,255,255,.15);overflow:hidden}' +
'.sss-fill{height:100%;border-radius:inherit;background:linear-gradient(90deg,var(--sss-a),var(--sss-b));transform-origin:0 50%;transform:scaleX(0)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function pad(n) { return (n < 10 ? '0' : '') + n; }
function parseTime(v) {
if (!v) return DEFAULTS.time;
if (v.indexOf(':') < 0) return Math.max(1, +v || DEFAULTS.time);
var p = v.split(':').map(Number);
return p.reduce(function (a, b) { return a * 60 + (b || 0); }, 0) || DEFAULTS.time;
}
function setup(el) {
var d = el.dataset;
var still = reduced();
var colors = (d.colors || DEFAULTS.colors).split(',').map(function (c) { return c.trim(); });
el.classList.add('sss');
el.style.setProperty('--sss-a', colors[0]);
el.style.setProperty('--sss-b', colors[1] || colors[0]);
// Background glows.
var bg = document.createElement('div');
bg.className = 'sss-bg';
bg.setAttribute('aria-hidden', 'true');
var orbs = colors.map(function (c) {
var o = document.createElement('span');
o.className = 'sss-orb';
o.style.background = 'radial-gradient(closest-side,' + c + ',transparent)';
bg.appendChild(o);
return o;
});
// Your content, then the countdown and its bar.
var body = document.createElement('div');
body.className = 'sss-body';
while (el.firstChild) body.appendChild(el.firstChild);
var clock = document.createElement('div');
clock.className = 'sss-clock';
var time = document.createElement('span');
time.className = 'sss-time';
time.setAttribute('role', 'timer');
var bars = [];
var eq = null;
if (d.eq !== 'false') {
eq = document.createElement('span');
eq.className = 'sss-eq';
eq.setAttribute('aria-hidden', 'true');
for (var i = 0; i < 4; i++) { var b = document.createElement('i'); eq.appendChild(b); bars.push(b); }
clock.appendChild(eq);
}
clock.appendChild(time);
body.appendChild(clock);
var track = document.createElement('div');
track.className = 'sss-track';
track.setAttribute('aria-hidden', 'true');
var fill = document.createElement('div');
fill.className = 'sss-fill';
track.appendChild(fill);
body.appendChild(track);
el.insertBefore(bg, el.firstChild);
el.appendChild(body);
var loop = d.loop !== undefined && d.loop !== 'false';
var total, endAt, lastSec = -1, finished = false;
function start() {
if (d.until) {
endAt = new Date(d.until).getTime();
total = Math.max(1, Math.round((endAt - Date.now()) / 1000));
} else {
total = parseTime(d.time);
endAt = Date.now() + total * 1000;
}
lastSec = -1;
finished = false;
time.innerHTML = '<span></span>:<span></span>';
}
function tick() {
var left = Math.max(0, Math.ceil((endAt - Date.now()) / 1000));
if (left === lastSec) return;
lastSec = left;
if (left === 0) {
if (finished) return;
finished = true;
time.textContent = d.done || DEFAULTS.done;
fill.style.transform = 'scaleX(1)';
if (!still) anime.animate(time, { scale: [0.6, 1], opacity: [0, 1], duration: 800, ease: 'outElastic(1, .6)' });
if (loop) setTimeout(start, 4000);
return;
}
var m = time.firstChild, s = time.lastChild;
m.textContent = pad(Math.floor(left / 60));
s.textContent = pad(left % 60);
fill.style.transform = 'scaleX(' + (1 - left / total).toFixed(4) + ')';
if (!still) anime.animate(s, { y: ['-0.2em', '0em'], opacity: [0.4, 1], duration: 400, ease: 'out(3)' });
}
start();
tick();
setInterval(tick, 250);
if (still) {
orbs.forEach(function (o, i) { place(o, el, i, orbs.length, false); });
return;
}
// The glows wander about, each on its own slow path.
var anims = [];
function size() { orbs.forEach(function (o, i) { place(o, el, i, orbs.length, true); }); }
size();
if (window.ResizeObserver) new ResizeObserver(size).observe(el);
orbs.forEach(function (o) {
anims.push(anime.animate(o, {
x: function () { return anime.utils.random(-0.25, 0.25, 2) * el.clientWidth; },
y: function () { return anime.utils.random(-0.25, 0.25, 2) * el.clientHeight; },
scale: function () { return anime.utils.random(0.8, 1.25, 2); },
duration: function () { return anime.utils.random(5000, 8000); },
ease: 'inOut(2)',
loop: true,
alternate: true,
autoplay: false
}));
});
// The equaliser: each bar jumps to a new level a few times a second.
if (bars.length) {
anims.push(anime.createTimer({
duration: 180,
loop: true,
autoplay: false,
onLoop: function () {
bars.forEach(function (b) {
anime.animate(b, { scaleY: anime.utils.random(0.2, 1, 2), duration: 170, ease: 'out(2)' });
});
}
}));
}
new IntersectionObserver(function (entries) {
anims.forEach(function (a) { if (entries[0].isIntersecting) a.play(); else a.pause(); });
}).observe(el);
}
// Spread the glows around the frame; they drift from these spots.
function place(o, el, i, n, moving) {
var w = el.clientWidth, h = el.clientHeight;
var r = Math.max(w, h) * 0.55;
var a = (i / n) * Math.PI * 2 + 0.6;
o.style.width = o.style.height = r + 'px';
o.style.left = (w / 2 + Math.cos(a) * w * 0.28 - r / 2) + 'px';
o.style.top = (h / 2 + Math.sin(a) * h * 0.3 - r / 2) + 'px';
if (!moving) o.style.transform = 'none';
}
function init() {
document.querySelectorAll('[data-starting-soon-screen]').forEach(function (el) {
if (el._sss) return;
el._sss = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-starting-soon-screen data-time="5:00"
style="height: 100vh">
<h2>Starting soon</h2>
<p>Grab a drink, we'll be right with you</p>
</div>