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.
- Cold open
- Headlines
- Studio interview
- Live music
- Wrap-up
How to use it
- Paste the snippet just before
</head>. - Add
data-show-rundownto an<ol>of the items, each with its length indata-timeif you like. - Call
animacious.showRundown(el)as each item ends, withdata-manualso it doesn’t move on by itself. It fills its container’s width; size it withfont-size.
Options
Add any of these attributes to the same element.
data-title | A heading above the list, with a blinking red dot. |
data-step | How long each item runs when it moves on by itself. Default 2400 ms. |
data-manual | Don’t move on by itself; use the script call. |
data-loop | Clear the ticks and start again after the last item. |
data-label | The tag on the current item. Default “On air”. |
data-color | Highlight 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.
<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><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>