← Broadcast

Show Rundown

The running order for a show or stream. A highlight with an “On air” tag sits on the current item while a progress bar fills along it. When the item is done its tick draws in, it is struck through and dimmed, and the highlight springs down to the next one.

  1. Cold open
  2. Headlines
  3. Studio interview
  4. Live music
  5. Wrap-up

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-show-rundown to an <ol> of the items, each with its length in data-time if you like.
  3. Call animacious.showRundown(el) as each item ends, with data-manual so it doesn’t move on by itself. It fills its container’s width; size it with font-size.

Options

Add any of these attributes to the same element.

data-titleA heading above the list, with a blinking red dot.
data-stepHow long each item runs when it moves on by itself. Default 2400 ms.
data-manualDon’t move on by itself; use the script call.
data-loopClear the ticks and start again after the last item.
data-labelThe tag on the current item. Default “On air”.
data-colorHighlight colour. Default #a855f7.

animacious.showRundown(el, 2) jumps straight to the third item, with everything before it ticked. The current item is marked with aria-current.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Show Rundown: a running order for a show or stream. The current item is
    // marked by a highlight with an "On air" tag and a progress bar that fills
    // as it runs; when it is done its tick draws in, it dims and the highlight
    // slides down to the next item.
    // Usage: <ol data-show-rundown data-title="Tonight">
    //          <li data-time="5:00">Welcome and news</li>
    //          <li data-time="12:00">Interview</li>
    //        </ol>
    // Each <li> is one item; data-time is shown on its right. Size it with
    // font-size; it fills the width of its container.
    // Optional: data-title (the heading; leave it out for no heading),
    // data-step (ms each item runs for in the demo, default 2400), data-color
    // (highlight colour, default #a855f7), data-label (the tag on the current
    // item, default "On air"), data-loop (start again from the top after the
    // last item; without it the rundown stops at the end), data-manual (don't
    // advance by itself; use the script call).
    // Script use: animacious.showRundown(el) ticks off the current item and
    // moves on; animacious.showRundown(el, 2) jumps to the third item.
    var DEFAULTS = { step: 2400, color: '#a855f7', label: 'On air' };

    var css =
      '.srd{position:relative;display:block;margin:0;padding:0.55em;list-style:none;line-height:1.2;color:#fff;--srd-c:' + DEFAULTS.color + ';' +
        'background:rgba(15,23,42,.82);border:1px solid rgba(255,255,255,.12);border-radius:0.8em;box-shadow:0 1em 2.5em rgba(0,0,0,.35)}' +
      '.srd-head{display:flex;align-items:center;justify-content:space-between;gap:1em;padding:0.3em 0.55em 0.6em;font-size:0.72em;font-weight:800;letter-spacing:0.14em;text-transform:uppercase;color:rgba(255,255,255,.6)}' +
      '.srd-head b{display:inline-flex;align-items:center;gap:0.5em;color:#fff}' +
      '.srd-head b::before{content:"";width:0.6em;height:0.6em;border-radius:50%;background:#ef4444;box-shadow:0 0 0.6em #ef4444;animation:srd-blink 1.6s ease-in-out infinite}' +
      '@keyframes srd-blink{50%{opacity:.35}}' +
      '.srd-hl{position:absolute;left:0.55em;right:0.55em;top:0;border-radius:0.55em;pointer-events:none;overflow:hidden;' +
        'background:color-mix(in srgb,var(--srd-c) 26%,transparent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--srd-c) 70%,transparent),0 0 1.2em color-mix(in srgb,var(--srd-c) 35%,transparent)}' +
      '.srd-bar{position:absolute;left:0;bottom:0;height:0.16em;width:100%;background:var(--srd-c);transform-origin:0 50%;transform:scaleX(0)}' +
      '.srd-row{position:relative;display:flex;align-items:center;gap:0.7em;padding:0.55em 0.6em;border-radius:0.55em;transition:opacity .5s}' +
      '.srd-mark{position:relative;flex:none;width:1.35em;height:1.35em;display:grid;place-items:center;border-radius:50%;font-size:0.8em;font-weight:800;' +
        'background:rgba(255,255,255,.1);color:rgba(255,255,255,.75);transition:background .35s,color .35s}' +
      '.srd-mark svg{position:absolute;inset:0;width:100%;height:100%;fill:none;stroke:#fff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}' +
      '.srd-mark path{stroke-dasharray:20;stroke-dashoffset:20}' +
      '.srd-done .srd-mark{background:#22c55e;color:transparent}' +
      '.srd-done{opacity:.5}' +
      '.srd-now .srd-mark{background:var(--srd-c);color:#fff}' +
      '.srd-title{position:relative;flex:1 1 auto;min-width:0;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
      '.srd-title::after{content:"";position:absolute;left:0;top:55%;height:0.09em;width:100%;background:currentColor;transform-origin:0 50%;transform:scaleX(var(--srd-x,0));transition:transform .45s ease-out}' +
      '.srd-done .srd-title{--srd-x:1}' +
      '.srd-tag{flex:none;padding:0.2em 0.5em;border-radius:0.3em;background:#ef4444;font-size:0.62em;font-weight:900;letter-spacing:0.1em;text-transform:uppercase;opacity:0;transform:scale(.6);transition:opacity .3s,transform .4s cubic-bezier(.3,1.6,.5,1)}' +
      '.srd-now .srd-tag{opacity:1;transform:none}' +
      '.srd-time{flex:none;font-size:0.82em;font-weight:600;font-variant-numeric:tabular-nums;color:rgba(255,255,255,.6)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var TICK = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5.5 10.5l3 3 6-7"/></svg>';

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
    function flag(el, k) { var v = el.dataset[k]; return v !== undefined && v !== 'false'; }

    function setup(el) {
      var d = el.dataset;
      var items = [].slice.call(el.children);
      if (!items.length) return false;
      var data = items.map(function (li) { return { text: li.textContent.trim(), time: li.dataset.time || '' }; });
      el.classList.add('srd');
      el.style.setProperty('--srd-c', d.color || DEFAULTS.color);
      el.innerHTML = '';
      if (d.title) {
        var head = document.createElement('li');
        head.className = 'srd-head';
        head.setAttribute('aria-hidden', 'true');
        head.innerHTML = '<b></b><span>' + data.length + ' items</span>';
        head.querySelector('b').textContent = d.title;
        el.appendChild(head);
        el.setAttribute('aria-label', d.title);
      }
      var hl = document.createElement('li');
      hl.className = 'srd-hl';
      hl.setAttribute('aria-hidden', 'true');
      hl.innerHTML = '<span class="srd-bar"></span>';
      el.appendChild(hl);
      var rows = data.map(function (it, i) {
        var row = document.createElement('li');
        row.className = 'srd-row';
        row.innerHTML = '<span class="srd-mark" aria-hidden="true">' + (i + 1) + TICK + '</span><span class="srd-title"></span>' +
          '<span class="srd-tag" aria-hidden="true"></span>' + (it.time ? '<span class="srd-time"></span>' : '');
        row.querySelector('.srd-title').textContent = it.text;
        row.querySelector('.srd-tag').textContent = d.label || DEFAULTS.label;
        if (it.time) row.querySelector('.srd-time').textContent = it.time;
        el.appendChild(row);
        return row;
      });
      var s = {
        rows: rows, hl: hl, bar: hl.querySelector('.srd-bar'),
        ticks: rows.map(function (r) { return r.querySelector('.srd-mark path'); }),
        index: -1, vis: false
      };
      el._srd = s;
      s.ro = new ResizeObserver(function () { if (s.index >= 0) place(el, false); });
      s.ro.observe(el);
      if (reduced()) { goTo(el, 0, true); return true; }
      anime.utils.set(s.hl, { opacity: 0 });
      return true;
    }

    // Put the highlight on the current row.
    function place(el, animate) {
      var s = el._srd, row = s.rows[s.index];
      var t = row.offsetTop, h = row.offsetHeight;
      if (animate) anime.animate(s.hl, { y: t, height: h, opacity: 1, duration: 600, ease: 'outElastic(1, .8)' });
      else anime.utils.set(s.hl, { y: t, height: h, opacity: 1 });
    }

    function goTo(el, i, instant) {
      var s = el._srd;
      i = Math.max(0, Math.min(s.rows.length - 1, i));
      s.rows.forEach(function (r, k) {
        r.classList.toggle('srd-done', k < i);
        r.classList.toggle('srd-now', k === i);
        r.removeAttribute('aria-current');
        anime.utils.set(s.ticks[k], { strokeDashoffset: k < i ? 0 : 20 });
      });
      s.rows[i].setAttribute('aria-current', 'step');
      s.index = i;
      place(el, !instant && !reduced());
      if (s.barAnim) s.barAnim.pause();
      anime.utils.set(s.bar, { scaleX: 0 });
    }

    // Finish the current item: fill its bar, tick it, move on.
    function advance(el) {
      var s = el._srd, i = s.index, row = s.rows[i];
      if (s.barAnim) s.barAnim.pause();
      var last = i === s.rows.length - 1;
      anime.animate(s.bar, { scaleX: 1, duration: 200, ease: 'out(2)' });
      row.classList.remove('srd-now');
      row.classList.add('srd-done');
      row.removeAttribute('aria-current');
      anime.animate(s.ticks[i], { strokeDashoffset: [20, 0], duration: 380, delay: 120, ease: 'out(2)' });
      anime.animate(row.querySelector('.srd-mark'), { scale: [1, 1.3, 1], duration: 500, ease: 'outBack(2)' });
      if (last) {
        anime.animate(s.hl, { opacity: 0, duration: 400, delay: 300 });
        s.index = s.rows.length;
        return false;
      }
      setTimeout(function () {
        s.index = i + 1;
        s.rows[s.index].classList.add('srd-now');
        s.rows[s.index].setAttribute('aria-current', 'step');
        anime.utils.set(s.bar, { scaleX: 0 });
        place(el, true);
      }, 260);
      return true;
    }

    function run(el) {
      var s = el._srd;
      var step = +(el.dataset.step || DEFAULTS.step);
      clearTimeout(s.wait);
      if (!s.vis) { s.pending = true; return; }
      if (s.index >= s.rows.length) {
        if (!flag(el, 'loop')) return;
        // Start again: clear the ticks, back to the top.
        s.rows.forEach(function (r) { r.classList.remove('srd-done'); });
        goTo(el, 0);
      }
      if (s.index < 0) goTo(el, 0);
      s.barAnim = anime.animate(s.bar, { scaleX: [0, 0.96], duration: step - 300, ease: 'linear' });
      s.wait = setTimeout(function () {
        advance(el);
        s.wait = setTimeout(function () { run(el); }, s.index >= s.rows.length ? 1600 : 700);
      }, step - 300);
    }

    window.animacious = window.animacious || {};
    window.animacious.showRundown = function (el, i) {
      if (!el || !el._srd) return;
      var s = el._srd;
      if (typeof i === 'number') { goTo(el, i); return; }
      if (s.index < 0) { goTo(el, 0); return; }
      if (s.index < s.rows.length) advance(el);
    };

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var el = e.target, s = el._srd;
          s.vis = e.isIntersecting;
          if (!s.vis) return;
          if (!s.started) {
            s.started = true;
            goTo(el, 0);
            if (!flag(el, 'manual')) s.wait = setTimeout(function () { run(el); }, 500);
          } else if (s.pending) { s.pending = false; run(el); }
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-show-rundown]').forEach(function (el) {
        if (el._srd) return;
        if (!setup(el)) return;
        if (!reduced()) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<ol data-show-rundown data-title="Tonight" data-manual>
  <li data-time="0:30">Cold open</li>
  <li data-time="4:00">Headlines</li>
  <li data-time="12:00">Studio interview</li>
</ol>

<script>
  // when an item ends:
  animacious.showRundown(document.querySelector('[data-show-rundown]'));
</script>