← Broadcast

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

  1. Paste the snippet just before </head>.
  2. Add data-now-playing to an element holding the song title and then the artist. Add more pairs for more songs.
  3. Point data-art at the cover image, or leave it out for a colourful generated cover. Size it with font-size.

Options

Add any of these attributes to the same element.

data-artAlbum art URL. Several, comma-separated, give one per song.
data-colorEqualiser and progress bar colour. Default #a855f7.
data-labelThe small heading. Default “Now playing”.
data-lengthSong length as m:ss, for the progress bar. Default 3:30.
data-progress="false"Leave out the progress bar.
data-loopLeave and come back with the next song, over and over.
data-holdHow long it stays before leaving, when looping. Default 4200 ms.

Call animacious.nowPlaying(el, 'Title', 'Artist', 'cover.jpg') when the song changes.

1 · Snippet
<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>
2 · Your HTML
<div data-now-playing data-art="cover.jpg" data-length="3:12">
  <strong>Midnight Drive</strong>
  <span>Neon Coast</span>
</div>