Now Playing
A music card for a stream. The album art pops in, a panel wipes out from behind it, the title and artist slide up into place, a small equaliser bounces beside “Now playing” and a progress bar creeps along. Give it a few songs and each loop shows the next.
Midnight DriveNeon CoastPaper PlanesThe Low TidesGolden HourMira Vale
How to use it
- Paste the snippet just before
</head>. - Add
data-now-playingto an element holding the song title and then the artist. Add more pairs for more songs. - Point
data-artat the cover image, or leave it out for a colourful generated cover. Size it withfont-size.
Options
Add any of these attributes to the same element.
data-art | Album art URL. Several, comma-separated, give one per song. |
data-color | Equaliser and progress bar colour. Default #a855f7. |
data-label | The small heading. Default “Now playing”. |
data-length | Song length as m:ss, for the progress bar. Default 3:30. |
data-progress="false" | Leave out the progress bar. |
data-loop | Leave and come back with the next song, over and over. |
data-hold | How long it stays before leaving, when looping. Default 4200 ms. |
Call animacious.nowPlaying(el, 'Title', 'Artist', 'cover.jpg') when the song changes.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Now Playing: a music overlay for a stream. The album art pops in, a panel
// wipes out from behind it, the song title and artist slide in, a little
// equaliser bounces beside the "Now playing" label and a progress bar
// creeps along. Give it several songs and each loop shows the next one.
// Usage: <div data-now-playing><strong>Midnight Drive</strong><span>Neon Coast</span></div>
// Children come in pairs: the title, then the artist. Size it with font-size.
// Optional: data-art (album art image URL; several, comma-separated, one
// per song; without it each song gets a colourful generated cover),
// data-color (equaliser and progress colour, default #a855f7), data-label
// (small heading, default "Now playing"), data-length (song length as m:ss,
// default 3:30), data-progress="false" (no progress bar), data-loop (leave
// and come back with the next song, over and over), data-hold (ms on
// screen before leaving, default 4200), data-gap (ms off screen between
// loops, default 700).
// Script use: animacious.nowPlaying(el, 'Title', 'Artist', 'cover.jpg') shows a new song.
var DEFAULTS = { color: '#a855f7', label: 'Now playing', length: '3:30', hold: 4200, gap: 700 };
var HUES = [280, 200, 330, 25, 160];
var css =
'.npl{display:inline-block;line-height:1.2;text-align:left;--npl-c:' + DEFAULTS.color + '}' +
'.npl-row{display:flex;align-items:center}' +
'.npl-art{position:relative;z-index:1;flex:none;width:4.6em;height:4.6em;border-radius:0.5em;overflow:hidden;background:#333 center/cover no-repeat;' +
'box-shadow:0 0.5em 1.4em rgba(0,0,0,.5),0 0 0 1px rgba(255,255,255,.12)}' +
'.npl-art::after{content:"";position:absolute;inset:0;background:linear-gradient(135deg,rgba(255,255,255,.28),transparent 45%)}' +
'.npl-disc{position:absolute;left:50%;top:50%;width:1.6em;height:1.6em;margin:-0.8em;border-radius:50%;background:radial-gradient(circle,#fff 0 12%,rgba(0,0,0,.65) 13% 100%);box-shadow:0 0 0 0.12em rgba(255,255,255,.35)}' +
'.npl-panel{position:relative;margin-left:-1em;padding:0.55em 1.1em 0.6em 1.75em;min-width:0;max-width:16em}' +
'.npl-bg{position:absolute;inset:0;border-radius:0 0.6em 0.6em 0;background:rgba(15,15,25,.86);border:1px solid rgba(255,255,255,.1);border-left:0;transform-origin:0 50%;' +
'box-shadow:0 0.5em 1.4em rgba(0,0,0,.35)}' +
'.npl-in{position:relative;display:flex;flex-direction:column;gap:0.1em;color:#fff}' +
'.npl-head{display:flex;align-items:flex-end;gap:0.45em;font-size:0.62em;font-weight:700;letter-spacing:0.16em;text-transform:uppercase;color:color-mix(in srgb,var(--npl-c) 45%,#fff)}' +
'.npl-eq{display:inline-flex;align-items:flex-end;gap:0.16em;height:1.4em}' +
'.npl-eq i{display:block;width:0.3em;height:100%;border-radius:0.1em;background:var(--npl-c);transform-origin:50% 100%;transform:scaleY(.35);animation:npl-eq .8s ease-in-out infinite}' +
'.npl-eq i:nth-child(2){animation-duration:.55s;animation-delay:-.2s}' +
'.npl-eq i:nth-child(3){animation-duration:.95s;animation-delay:-.45s}' +
'.npl-eq i:nth-child(4){animation-duration:.66s;animation-delay:-.1s}' +
'.npl-still .npl-eq i{animation-play-state:paused}' +
'@keyframes npl-eq{0%,100%{transform:scaleY(.25)}50%{transform:scaleY(1)}}' +
'.npl-clip{display:block;overflow:hidden;padding-bottom:0.08em}' +
'.npl-title{display:block;font-weight:800;font-size:1.15em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.npl-artist{display:block;font-size:0.85em;opacity:.72;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
'.npl-prog{display:flex;align-items:center;gap:0.5em;margin-top:0.35em;font-size:0.6em;font-variant-numeric:tabular-nums;opacity:.75}' +
'.npl-track{position:relative;flex:1;min-width:4em;height:0.35em;border-radius:1em;background:rgba(255,255,255,.18);overflow:hidden}' +
'.npl-fill{position:absolute;inset:0;background:var(--npl-c);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 num(el, k) { var v = el.dataset[k]; return v !== undefined && v !== '' ? +v : DEFAULTS[k]; }
function secs(t) {
var p = String(t).split(':');
return p.length > 1 ? (+p[0] || 0) * 60 + (+p[1] || 0) : +t || 210;
}
function clock(t) { t = Math.max(0, Math.floor(t)); return Math.floor(t / 60) + ':' + ('0' + (t % 60)).slice(-2); }
function cover(i) {
var h = HUES[i % HUES.length];
return 'radial-gradient(circle at 30% 25%,hsl(' + (h + 40) + ' 90% 70%),transparent 55%),' +
'radial-gradient(circle at 80% 85%,hsl(' + (h - 50) + ' 85% 55%),transparent 60%),' +
'linear-gradient(135deg,hsl(' + h + ' 70% 45%),hsl(' + (h + 70) + ' 60% 20%))';
}
function setup(el) {
var d = el.dataset;
var kids = Array.prototype.slice.call(el.children);
var arts = (d.art || '').split(',').map(function (s) { return s.trim(); });
var tracks = [];
for (var i = 0; i < kids.length; i += 2) {
tracks.push({ title: kids[i].textContent.trim(), artist: kids[i + 1] ? kids[i + 1].textContent.trim() : '', art: arts[tracks.length] || '' });
}
if (!tracks.length) tracks.push({ title: el.textContent.trim() || 'Untitled', artist: '', art: arts[0] || '' });
el.classList.add('npl');
el.style.setProperty('--npl-c', d.color || DEFAULTS.color);
el.setAttribute('role', 'status');
var showProg = d.progress !== 'false';
el.innerHTML =
'<div class="npl-row" aria-hidden="true">' +
'<span class="npl-art"></span>' +
'<span class="npl-panel"><span class="npl-bg"></span><span class="npl-in">' +
'<span class="npl-head"><span class="npl-eq"><i></i><i></i><i></i><i></i></span><span class="npl-lbl"></span></span>' +
'<span class="npl-clip"><span class="npl-title"></span></span>' +
'<span class="npl-clip"><span class="npl-artist"></span></span>' +
(showProg ? '<span class="npl-prog"><span class="npl-now">0:00</span><span class="npl-track"><span class="npl-fill"></span></span><span class="npl-len"></span></span>' : '') +
'</span></span>' +
'</div>';
var s = {
tracks: tracks, index: 0, length: secs(d.length || DEFAULTS.length),
art: el.querySelector('.npl-art'), bg: el.querySelector('.npl-bg'),
head: el.querySelector('.npl-head'), title: el.querySelector('.npl-title'), artist: el.querySelector('.npl-artist'),
prog: el.querySelector('.npl-prog'), fill: el.querySelector('.npl-fill'),
now: el.querySelector('.npl-now'), len: el.querySelector('.npl-len')
};
el.querySelector('.npl-lbl').textContent = d.label || DEFAULTS.label;
if (s.len) s.len.textContent = clock(s.length);
el._npl = s;
setTrack(el, tracks[0]);
if (reduced()) { el.classList.add('npl-still'); setProgress(el, 0.35); return; }
hide(el);
}
function setTrack(el, t) {
var s = el._npl;
s.track = t;
s.title.textContent = t.title;
s.artist.textContent = t.artist;
s.art.style.backgroundImage = t.art ? 'url("' + t.art.replace(/"/g, '%22') + '")' : cover(Math.max(0, s.tracks.indexOf(t)));
s.art.innerHTML = t.art ? '' : '<span class="npl-disc"></span>';
el.setAttribute('aria-label', (el.dataset.label || DEFAULTS.label) + ': ' + t.title + (t.artist ? ' by ' + t.artist : ''));
}
function setProgress(el, f) {
var s = el._npl;
if (!s.fill) return;
s.fill.style.transform = 'scaleX(' + f + ')';
s.now.textContent = clock(f * s.length);
}
function hide(el) {
var s = el._npl;
anime.utils.set(s.art, { opacity: 0, scale: 0.4, rotate: -10 });
anime.utils.set(s.bg, { scaleX: 0 });
anime.utils.set([s.head, s.prog].filter(Boolean), { opacity: 0, x: '-0.6em' });
anime.utils.set([s.title, s.artist], { y: '110%' });
}
function show(el) {
var s = el._npl;
if (s.tl) s.tl.pause();
clearTimeout(s.wait);
var loop = el.dataset.loop !== undefined && el.dataset.loop !== 'false';
hide(el);
el.classList.remove('npl-still');
// Start part-way through the song, and tick along in real time.
s.pos = 0.15 + Math.random() * 0.3;
s.t0 = performance.now();
setProgress(el, s.pos);
var tl = anime.createTimeline();
tl.add(s.art, { opacity: [0, 1], scale: [0.4, 1], rotate: [-10, 0], duration: 700, ease: 'outBack(1.8)' })
.add(s.bg, { scaleX: [0, 1], duration: 600, ease: 'out(4)' }, 300)
.add(s.head, { opacity: [0, 1], x: ['-0.6em', '0em'], duration: 450, ease: 'out(3)' }, 550)
.add(s.title, { y: ['110%', '0%'], duration: 550, ease: 'out(4)' }, 620)
.add(s.artist, { y: ['110%', '0%'], duration: 550, ease: 'out(4)' }, 720);
if (s.prog) tl.add(s.prog, { opacity: [0, 1], x: ['-0.6em', '0em'], duration: 450, ease: 'out(3)' }, 820);
if (loop) {
var out = 900 + num(el, 'hold');
tl.add([s.title, s.artist], { y: '-110%', duration: 380, ease: 'in(3)', delay: anime.stagger(60) }, out)
.add([s.head, s.prog].filter(Boolean), { opacity: 0, duration: 300, ease: 'linear' }, out)
.add(s.bg, { scaleX: 0, duration: 420, ease: 'in(3)' }, out + 250)
.add(s.art, { opacity: 0, scale: 0.6, rotate: 8, duration: 360, ease: 'in(3)' }, out + 520);
tl.then(function () {
s.wait = setTimeout(function () {
s.index = (s.index + 1) % s.tracks.length;
setTrack(el, s.tracks[s.index]);
if (s.visible) show(el); else s.pending = true;
}, num(el, 'gap'));
});
}
s.tl = tl;
tick(el);
}
function tick(el) {
var s = el._npl;
cancelAnimationFrame(s.raf);
if (!s.fill || !s.visible) return;
var f = s.pos + (performance.now() - s.t0) / 1000 / s.length;
if (f >= 1) { s.pos = 0; s.t0 = performance.now(); f = 0; }
s.cur = f;
setProgress(el, f);
s.raf = requestAnimationFrame(function () { tick(el); });
}
window.animacious = window.animacious || {};
window.animacious.nowPlaying = function (el, title, artist, art) {
if (!el || !el._npl) return;
var s = el._npl;
if (title) {
var t = { title: title, artist: artist || '', art: art || '' };
s.tracks.push(t);
s.index = s.tracks.length - 1;
setTrack(el, t);
}
if (!reduced()) show(el);
};
function init() {
// Off screen the equaliser and progress bar stop, and loops wait.
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var el = e.target, s = el._npl;
s.visible = e.isIntersecting;
el.classList.toggle('npl-still', !e.isIntersecting);
if (!e.isIntersecting) { cancelAnimationFrame(s.raf); if (s.cur !== undefined) s.pos = s.cur; return; }
if (!s.started) { s.started = true; setTimeout(function () { show(el); }, 200); }
else if (s.pending) { s.pending = false; show(el); }
else { s.t0 = performance.now(); tick(el); }
});
}, { threshold: 0.1 });
document.querySelectorAll('[data-now-playing]').forEach(function (el) {
if (el._npl) return;
setup(el);
if (!reduced()) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-now-playing data-art="cover.jpg" data-length="3:12">
<strong>Midnight Drive</strong>
<span>Neon Coast</span>
</div>