← Broadcast

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

  1. Paste the snippet just before </head>.
  2. Add data-starting-soon-screen to a box with a height, holding your heading and any message.
  3. Set the countdown with data-time="5:00", or count down to a real start with data-until.

Options

Add any of these attributes to the same element.

data-timeCountdown length, as m:ss or seconds. Default 5:00.
data-untilA date and time to count down to instead, e.g. 2026-12-01T19:00.
data-doneText shown at zero. Default “Starting now”.
data-colorsComma-separated glow colours. Default purple, pink, blue and indigo.
data-loopStart 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.

1 · Snippet
<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>
2 · Your HTML
<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>