Open demo

CSS code

HTML
<div><span class="lbl">Sticky mini player bar</span>
<div class="mw mw-dock" data-mw data-length="24" role="group" aria-label="Paper Kites by The Lumen Choir">
  <audio src="../audio/track-2.mp3" preload="metadata"></audio>
  <div class="mw-art" style="--art:linear-gradient(to bottom right,transparent 50%,#fde047 50%) 37.5% 26.67%/20% 25% no-repeat,linear-gradient(to bottom left,transparent 50%,#f97316 50%) 62.5% 26.67%/20% 25% no-repeat,linear-gradient(to top right,transparent 50%,#fb923c 50%) 37.5% 69.23%/20% 35% no-repeat,linear-gradient(to top left,transparent 50%,#facc15 50%) 62.5% 69.23%/20% 35% no-repeat,linear-gradient(to bottom right,transparent calc(50% - 1px),rgba(255,255,255,.85) 0 calc(50% + 1px),transparent 0) 41.86% 100%/14% 20% no-repeat,linear-gradient(160deg,#0ea5e9,#14b8a6 55%,#84cc16)" aria-hidden="true"></div>
  <div class="mw-text"><p class="mw-title">Paper Kites</p><p class="mw-artist">The Lumen Choir</p></div>
  <span class="mw-time"><span data-cur>0:00</span> / <span data-dur>0:24</span></span>
  <button type="button" class="mw-icon mw-mute" data-mute aria-label="Mute" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path class="i-wave" d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/><path class="i-x" d="m16 9 6 6m0-6-6 6"/></svg></button>
  <button type="button" class="mw-play" data-play="Paper Kites" aria-label="Play Paper Kites"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
  <div class="mw-line"><input type="range" data-seek min="0" max="24" step="any" value="0" aria-label="Seek Paper Kites" aria-valuetext="0:00 of 0:24"></div>
</div></div>

<div><span class="lbl">Pill player</span>
<div class="mw mw-pill" data-mw data-length="30" role="group" aria-label="Night Bus to Nowhere by Orla Finch">
  <audio src="../audio/track-3.mp3" preload="metadata"></audio>
  <button type="button" class="mw-play" data-play="Night Bus to Nowhere" aria-label="Play Night Bus to Nowhere"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
  <div class="mw-text"><p class="mw-title">Night Bus to Nowhere · Orla Finch</p></div>
  <span class="mw-time" data-cur>0:00</span>
  <div class="mw-art" style="--art:radial-gradient(circle at 68% 30%,#fef3c7 0 9%,rgba(254,243,199,.16) 9.5% 15%,transparent 15.5%),radial-gradient(circle at 20% 25%,#fff 0 1.2%,transparent 2%),radial-gradient(circle at 38% 55%,#fff 0 .9%,transparent 1.6%),radial-gradient(circle at 86% 64%,#fff 0 1%,transparent 1.7%),radial-gradient(circle at 12% 70%,#c4b5fd 0 1%,transparent 1.7%),linear-gradient(180deg,#0f172a,#312e81 70%,#7c3aed)" aria-hidden="true"></div>
</div></div>

<div><span class="lbl">Floating ring widget</span>
<div class="mw mw-float" data-mw data-length="29" role="group" aria-label="Copper Sky by Milo & the Tidelines">
  <audio src="../audio/track-4.mp3" preload="metadata"></audio>
  <div class="mw-ring" style="--art:radial-gradient(circle at 50% 100%,#fff7ed 0 24%,#fdba74 24% 27%,transparent 27.5%),linear-gradient(180deg,#7c2d12 0 22%,#9a3412 22% 40%,#c2410c 40% 56%,#ea580c 56% 70%,#f59e0b 70% 84%,#fcd34d 84%)">
    <button type="button" class="mw-play" data-play="Copper Sky" aria-label="Play Copper Sky"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
  </div>
  <div class="mw-text"><p class="mw-title">Copper Sky</p><p class="mw-artist">Milo & the Tidelines</p></div>
  <span class="mw-time" data-left>0:29 left</span>
</div></div>
CSS
/* Mini and floating players: small footprints around a native <audio>.
   The script sets --p (0 to 1) on .mw, toggles [data-playing] and writes the time. */
.mw{--mw-accent:#7c3aed;--mw-soft:#ede9fe;--mw-ink:#1e1b2e;--mw-muted:#5b5675;--mw-track:#8b84a3;--p:0;color:var(--mw-ink)}
.mw button{flex:none;display:grid;place-items:center;border:0;background:none;color:inherit;cursor:pointer;font:inherit}
.mw button:focus-visible{outline:2px solid var(--mw-accent);outline-offset:2px}
.mw svg{display:block;width:18px;height:18px}
.mw .mw-play{width:40px;height:40px;border-radius:50%;background:var(--mw-accent);color:#fff}
.mw .i-pause,.mw[data-playing] .i-play{display:none}
.mw[data-playing] .i-pause{display:block}
.mw .mw-icon{width:36px;height:36px;border-radius:50%;color:var(--mw-muted)}
.mw .mw-icon:hover{background:var(--mw-soft)}
.mw-mute .i-x,.mw-mute[aria-pressed="true"] .i-wave{display:none}
.mw-mute[aria-pressed="true"] .i-x{display:block}
.mw-art{flex:none;background:var(--art)}
.mw-text{flex:1;min-width:0}
.mw-title{font-weight:700;font-size:.92rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mw-artist{font-size:.8rem;color:var(--mw-muted);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mw-time{font-size:.78rem;color:var(--mw-muted);font-variant-numeric:tabular-nums;white-space:nowrap}
/* 1. sticky mini player: docks to the bottom of the viewport while the page scrolls */
.mw-dock{position:sticky;bottom:0;z-index:10;display:flex;align-items:center;gap:12px;padding:14px 14px 12px;
  background:#fff;border:1px solid #e4e0f3;border-radius:16px;box-shadow:0 -4px 24px rgba(30,27,46,.08)}
.mw-dock .mw-art{width:44px;height:44px;border-radius:10px}
.mw-line{position:absolute;left:14px;right:14px;top:-7px}
.mw-line input{-webkit-appearance:none;appearance:none;display:block;width:100%;height:14px;margin:0;background:none;cursor:pointer}
.mw-line input:focus-visible{outline:none}
.mw-line input::-webkit-slider-runnable-track{height:4px;border-radius:999px;
  background:linear-gradient(to right,var(--mw-accent) 0 calc(7px + (100% - 14px) * var(--p)),var(--mw-track) 0)}
.mw-line input::-moz-range-track{height:4px;border-radius:999px;
  background:linear-gradient(to right,var(--mw-accent) 0 calc(7px + (100% - 14px) * var(--p)),var(--mw-track) 0)}
.mw-line input::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;margin-top:-5px;border:0;border-radius:50%;
  background:var(--mw-accent);opacity:0;transition:opacity .15s}
.mw-line input::-moz-range-thumb{width:14px;height:14px;border:0;border-radius:50%;background:var(--mw-accent);opacity:0;transition:opacity .15s}
.mw-line input:hover::-webkit-slider-thumb,.mw-line input:focus-visible::-webkit-slider-thumb{opacity:1}
.mw-line input:hover::-moz-range-thumb,.mw-line input:focus-visible::-moz-range-thumb{opacity:1}
.mw-line input:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--mw-accent)}
.mw-line input:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--mw-accent)}
/* 2. pill player: the background fills as the track plays */
.mw-pill{display:inline-flex;align-items:center;gap:10px;max-width:100%;padding:5px 16px 5px 5px;border:1px solid #d9d3f0;border-radius:999px;
  background:linear-gradient(to right,var(--mw-soft) calc(var(--p) * 100%),#fff 0)}
.mw-pill .mw-play{width:34px;height:34px}
.mw-pill .mw-art{width:34px;height:34px;border-radius:50%}
.mw-pill .mw-title{font-size:.88rem}
/* 3. floating widget with a progress ring; add position:fixed;right:20px;bottom:20px to float it */
.mw-float{display:flex;align-items:center;gap:14px;width:300px;max-width:100%;padding:12px 16px 12px 12px;background:#fff;border-radius:20px;
  box-shadow:0 2px 6px rgba(30,27,46,.06),0 18px 40px rgba(76,29,149,.16)}
.mw-ring{position:relative;flex:none;width:56px;height:56px;border-radius:50%;
  background:conic-gradient(var(--mw-accent) calc(var(--p) * 1turn),var(--mw-track) 0)}
.mw-ring .mw-play{position:absolute;inset:4px;width:auto;height:auto;background:var(--art);color:#fff}
.mw-ring .mw-play svg{width:20px;height:20px;filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))}
@media (max-width:420px){.mw-dock .mw-time{display:none}}
@media (prefers-reduced-motion:reduce){.mw-line input::-webkit-slider-thumb{transition:none}.mw-line input::-moz-range-thumb{transition:none}}
JavaScript
// Mini players. Each [data-mw] wraps one <audio>; the script wires whichever controls it contains
// (play button, seek slider, mute button) and writes the elapsed or remaining time.
(() => {
  const fmt = s => { s = Math.floor(s || 0); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
  document.querySelectorAll('[data-mw]').forEach(root => {
    const $ = s => root.querySelector(s);
    const audio = $('audio'), play = $('[data-play]'), seek = $('[data-seek]'), cur = $('[data-cur]'), dur = $('[data-dur]'), left = $('[data-left]');
    const mute = $('[data-mute]'), name = play.dataset.play;
    let raf = 0;
    const length = () => audio.duration || +root.dataset.length;
    const sync = () => {
      const t = audio.currentTime, d = length(), text = fmt(t) + ' of ' + fmt(d);
      root.style.setProperty('--p', d ? t / d : 0);
      if (cur) cur.textContent = fmt(t);
      if (left) left.textContent = fmt(d - t) + ' left';
      if (seek) {
        seek.value = t;
        if (seek.getAttribute('aria-valuetext') !== text) seek.setAttribute('aria-valuetext', text);
      }
    };
    const tick = () => { sync(); raf = requestAnimationFrame(tick); };
    const meta = () => { if (seek) seek.max = audio.duration; if (dur) dur.textContent = fmt(audio.duration); sync(); };
    play.addEventListener('click', () => {
      if (!audio.paused) return audio.pause();
      document.querySelectorAll('audio').forEach(a => { if (a !== audio) a.pause(); });
      audio.play().catch(() => {});
    });
    audio.addEventListener('play', () => { root.setAttribute('data-playing', ''); play.setAttribute('aria-label', 'Pause ' + name); tick(); });
    audio.addEventListener('pause', () => { root.removeAttribute('data-playing'); play.setAttribute('aria-label', 'Play ' + name); cancelAnimationFrame(raf); sync(); });
    audio.addEventListener('loadedmetadata', meta);
    if (audio.readyState > 0) meta();
    if (seek) seek.addEventListener('input', () => { audio.currentTime = seek.value; sync(); });
    // Arrow keys jump 5 seconds; Home and End work natively.
    if (seek) seek.addEventListener('keydown', e => {
      const step = { ArrowLeft: -5, ArrowDown: -5, ArrowRight: 5, ArrowUp: 5 }[e.key];
      if (!step) return;
      e.preventDefault();
      audio.currentTime = Math.min(Math.max(audio.currentTime + step, 0), length());
      sync();
    });
    if (mute) {
      mute.addEventListener('click', () => { audio.muted = !audio.muted; });
      audio.addEventListener('volumechange', () => mute.setAttribute('aria-pressed', String(audio.muted)));
    }
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Sticky mini player bar</span>
<div class="card mw mw-dock" data-mw data-length="24" role="group" aria-label="Paper Kites by The Lumen Choir">
  <div class="card-body">
  <audio src="../audio/track-2.mp3" preload="metadata"></audio>
  <div class="mw-art" style="--art:linear-gradient(to bottom right,transparent 50%,#fde047 50%) 37.5% 26.67%/20% 25% no-repeat,linear-gradient(to bottom left,transparent 50%,#f97316 50%) 62.5% 26.67%/20% 25% no-repeat,linear-gradient(to top right,transparent 50%,#fb923c 50%) 37.5% 69.23%/20% 35% no-repeat,linear-gradient(to top left,transparent 50%,#facc15 50%) 62.5% 69.23%/20% 35% no-repeat,linear-gradient(to bottom right,transparent calc(50% - 1px),rgba(255,255,255,.85) 0 calc(50% + 1px),transparent 0) 41.86% 100%/14% 20% no-repeat,linear-gradient(160deg,#0ea5e9,#14b8a6 55%,#84cc16)" aria-hidden="true"></div>
  <div class="flex-grow-1 text-truncate"><p class="mw-title mb-0 text-truncate">Paper Kites</p><p class="mw-artist mb-0 text-truncate">The Lumen Choir</p></div>
  <span class="mw-time"><span data-cur>0:00</span> / <span data-dur>0:24</span></span>
  <button type="button" class="btn mw-icon mw-mute rounded-circle" data-mute aria-label="Mute" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path class="i-wave" d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/><path class="i-x" d="m16 9 6 6m0-6-6 6"/></svg></button>
  <button type="button" class="btn mw-play rounded-circle" data-play="Paper Kites" aria-label="Play Paper Kites"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
  <div class="mw-line"><input type="range" class="form-range" data-seek min="0" max="24" step="any" value="0" aria-label="Seek Paper Kites" aria-valuetext="0:00 of 0:24"></div>
  </div>
</div></div>

<div><span class="lbl">Pill player</span>
<div class="mw mw-pill rounded-pill" data-mw data-length="30" role="group" aria-label="Night Bus to Nowhere by Orla Finch">
  <div class="progress" aria-hidden="true"><div class="progress-bar"></div></div>
  <audio src="../audio/track-3.mp3" preload="metadata"></audio>
  <button type="button" class="btn mw-play rounded-circle" data-play="Night Bus to Nowhere" aria-label="Play Night Bus to Nowhere"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
  <div class="flex-grow-1 text-truncate"><p class="mw-title mb-0 text-truncate">Night Bus to Nowhere · Orla Finch</p></div>
  <span class="mw-time" data-cur>0:00</span>
  <div class="mw-art" style="--art:radial-gradient(circle at 68% 30%,#fef3c7 0 9%,rgba(254,243,199,.16) 9.5% 15%,transparent 15.5%),radial-gradient(circle at 20% 25%,#fff 0 1.2%,transparent 2%),radial-gradient(circle at 38% 55%,#fff 0 .9%,transparent 1.6%),radial-gradient(circle at 86% 64%,#fff 0 1%,transparent 1.7%),radial-gradient(circle at 12% 70%,#c4b5fd 0 1%,transparent 1.7%),linear-gradient(180deg,#0f172a,#312e81 70%,#7c3aed)" aria-hidden="true"></div>
</div></div>

<div><span class="lbl">Floating ring widget</span>
<div class="card mw mw-float" data-mw data-length="29" role="group" aria-label="Copper Sky by Milo & the Tidelines">
  <div class="card-body">
  <audio src="../audio/track-4.mp3" preload="metadata"></audio>
  <div class="mw-ring" style="--art:radial-gradient(circle at 50% 100%,#fff7ed 0 24%,#fdba74 24% 27%,transparent 27.5%),linear-gradient(180deg,#7c2d12 0 22%,#9a3412 22% 40%,#c2410c 40% 56%,#ea580c 56% 70%,#f59e0b 70% 84%,#fcd34d 84%)">
    <button type="button" class="btn mw-play rounded-circle" data-play="Copper Sky" aria-label="Play Copper Sky"><svg class="i-play" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
  </div>
  <div class="flex-grow-1 text-truncate"><p class="mw-title mb-0 text-truncate">Copper Sky</p><p class="mw-artist mb-0 text-truncate">Milo & the Tidelines</p></div>
  <span class="mw-time" data-left>0:29 left</span>
  </div>
</div></div>
CSS
/* Mini and floating players on Bootstrap: .card and .rounded-pill shells, .btn controls, .form-range and .progress.
   The script sets --p (0 to 1) on .mw, toggles [data-playing] and writes the time. */
.mw{--mw-accent:#7c3aed;--mw-soft:#ede9fe;--mw-ink:#1e1b2e;--mw-muted:#5b5675;--mw-track:#8b84a3;--p:0;color:var(--mw-ink)}
.mw .btn{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-border-width:0;flex:none;display:grid;place-items:center}
.mw .btn:focus-visible{outline:2px solid var(--mw-accent);outline-offset:2px;box-shadow:none}
.mw svg{display:block;width:18px;height:18px}
.mw .mw-play{--bs-btn-bg:var(--mw-accent);--bs-btn-color:#fff;--bs-btn-hover-bg:#6d28d9;--bs-btn-hover-color:#fff;
  --bs-btn-active-bg:#5b21b6;--bs-btn-active-color:#fff;width:40px;height:40px}
.mw .i-pause,.mw[data-playing] .i-play{display:none}
.mw[data-playing] .i-pause{display:block}
.mw .mw-icon{--bs-btn-color:var(--mw-muted);--bs-btn-hover-bg:var(--mw-soft);--bs-btn-hover-color:var(--mw-muted);
  --bs-btn-active-bg:var(--mw-soft);--bs-btn-active-color:var(--mw-accent);width:36px;height:36px}
.mw-mute .i-x,.mw-mute[aria-pressed="true"] .i-wave{display:none}
.mw-mute[aria-pressed="true"] .i-x{display:block}
.mw-art{flex:none;background:var(--art)}
.mw-title{font-weight:700;font-size:.92rem}
.mw-artist{font-size:.8rem;color:var(--mw-muted);margin-top:2px}
.mw-time{font-size:.78rem;color:var(--mw-muted);font-variant-numeric:tabular-nums;white-space:nowrap}
/* 1. sticky mini player: docks to the bottom of the viewport while the page scrolls */
.mw-dock{position:sticky;bottom:0;z-index:10;--bs-card-border-color:#e4e0f3;--bs-card-border-radius:16px;box-shadow:0 -4px 24px rgba(30,27,46,.08)}
.mw-dock .card-body{display:flex;align-items:center;gap:12px;padding:14px 14px 12px}
.mw-dock .mw-art{width:44px;height:44px;border-radius:10px}
.mw-line{position:absolute;left:14px;right:14px;top:-7px}
.mw-line .form-range{display:block;height:14px}
.mw-line .form-range::-webkit-slider-runnable-track{height:4px;border-radius:999px;
  background:linear-gradient(to right,var(--mw-accent) 0 calc(7px + (100% - 14px) * var(--p)),var(--mw-track) 0)}
.mw-line .form-range::-moz-range-track{height:4px;border-radius:999px;
  background:linear-gradient(to right,var(--mw-accent) 0 calc(7px + (100% - 14px) * var(--p)),var(--mw-track) 0)}
.mw-line .form-range::-webkit-slider-thumb{width:14px;height:14px;margin-top:-5px;background:var(--mw-accent);opacity:0;transition:opacity .15s}
.mw-line .form-range::-moz-range-thumb{width:14px;height:14px;background:var(--mw-accent);opacity:0;transition:opacity .15s}
.mw-line .form-range:hover::-webkit-slider-thumb,.mw-line .form-range:focus-visible::-webkit-slider-thumb{opacity:1}
.mw-line .form-range:hover::-moz-range-thumb,.mw-line .form-range:focus-visible::-moz-range-thumb{opacity:1}
.mw-line .form-range:focus::-webkit-slider-thumb{box-shadow:none}
.mw-line .form-range:focus::-moz-range-thumb{box-shadow:none}
.mw-line .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--mw-accent)}
.mw-line .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--mw-accent)}
/* 2. pill player: a .progress behind the content fills as the track plays */
.mw-pill{position:relative;display:inline-flex;align-items:center;gap:10px;max-width:100%;padding:5px 16px 5px 5px;border:1px solid #d9d3f0;background:#fff;overflow:hidden}
.mw-pill .progress{position:absolute;inset:0;height:auto;border-radius:0;--bs-progress-bg:#fff;--bs-progress-bar-bg:var(--mw-soft);--bs-progress-bar-transition:none}
.mw-pill .progress-bar{width:calc(var(--p) * 100%)}
.mw-pill > :not(.progress){position:relative}
.mw-pill .mw-play{width:34px;height:34px}
.mw-pill .mw-art{width:34px;height:34px;border-radius:50%}
.mw-pill .mw-title{font-size:.88rem}
/* 3. floating widget with a progress ring; add position:fixed;right:20px;bottom:20px to float it */
.mw-float{width:300px;max-width:100%;--bs-card-border-width:0;--bs-card-border-radius:20px;
  box-shadow:0 2px 6px rgba(30,27,46,.06),0 18px 40px rgba(76,29,149,.16)}
.mw-float .card-body{display:flex;align-items:center;gap:14px;padding:12px 16px 12px 12px}
.mw-ring{position:relative;flex:none;width:56px;height:56px;border-radius:50%;
  background:conic-gradient(var(--mw-accent) calc(var(--p) * 1turn),var(--mw-track) 0)}
.mw-ring .mw-play{position:absolute;inset:4px;width:auto;height:auto;--bs-btn-color:#fff;--bs-btn-hover-color:#fff;--bs-btn-active-color:#fff;background:var(--art)}
.mw-ring .mw-play svg{width:20px;height:20px;filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))}
@media (max-width:420px){.mw-dock .mw-time{display:none}}
@media (prefers-reduced-motion:reduce){.mw-line .form-range::-webkit-slider-thumb{transition:none}.mw-line .form-range::-moz-range-thumb{transition:none}}
JavaScript
// Mini players. Each [data-mw] wraps one <audio>; the script wires whichever controls it contains
// (play button, seek slider, mute button) and writes the elapsed or remaining time.
(() => {
  const fmt = s => { s = Math.floor(s || 0); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
  document.querySelectorAll('[data-mw]').forEach(root => {
    const $ = s => root.querySelector(s);
    const audio = $('audio'), play = $('[data-play]'), seek = $('[data-seek]'), cur = $('[data-cur]'), dur = $('[data-dur]'), left = $('[data-left]');
    const mute = $('[data-mute]'), name = play.dataset.play;
    let raf = 0;
    const length = () => audio.duration || +root.dataset.length;
    const sync = () => {
      const t = audio.currentTime, d = length(), text = fmt(t) + ' of ' + fmt(d);
      root.style.setProperty('--p', d ? t / d : 0);
      if (cur) cur.textContent = fmt(t);
      if (left) left.textContent = fmt(d - t) + ' left';
      if (seek) {
        seek.value = t;
        if (seek.getAttribute('aria-valuetext') !== text) seek.setAttribute('aria-valuetext', text);
      }
    };
    const tick = () => { sync(); raf = requestAnimationFrame(tick); };
    const meta = () => { if (seek) seek.max = audio.duration; if (dur) dur.textContent = fmt(audio.duration); sync(); };
    play.addEventListener('click', () => {
      if (!audio.paused) return audio.pause();
      document.querySelectorAll('audio').forEach(a => { if (a !== audio) a.pause(); });
      audio.play().catch(() => {});
    });
    audio.addEventListener('play', () => { root.setAttribute('data-playing', ''); play.setAttribute('aria-label', 'Pause ' + name); tick(); });
    audio.addEventListener('pause', () => { root.removeAttribute('data-playing'); play.setAttribute('aria-label', 'Play ' + name); cancelAnimationFrame(raf); sync(); });
    audio.addEventListener('loadedmetadata', meta);
    if (audio.readyState > 0) meta();
    if (seek) seek.addEventListener('input', () => { audio.currentTime = seek.value; sync(); });
    // Arrow keys jump 5 seconds; Home and End work natively.
    if (seek) seek.addEventListener('keydown', e => {
      const step = { ArrowLeft: -5, ArrowDown: -5, ArrowRight: 5, ArrowUp: 5 }[e.key];
      if (!step) return;
      e.preventDefault();
      audio.currentTime = Math.min(Math.max(audio.currentTime + step, 0), length());
      sync();
    });
    if (mute) {
      mute.addEventListener('click', () => { audio.muted = !audio.muted; });
      audio.addEventListener('volumechange', () => mute.setAttribute('aria-pressed', String(audio.muted)));
    }
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-mw-muted uppercase">Sticky mini player bar</span>
<div class="group sticky bottom-0 z-10 flex items-center gap-3 rounded-2xl border border-[#e4e0f3] bg-white px-3.5 pt-3.5 pb-3 text-[#1e1b2e] shadow-[0_-4px_24px_rgba(30,27,46,.08)] [--p:0]" data-mw data-length="24" role="group" aria-label="Paper Kites by The Lumen Choir">
  <audio src="../audio/track-2.mp3" preload="metadata"></audio>
  <div class="size-11 flex-none rounded-[10px] [background:linear-gradient(to_bottom_right,transparent_50%,#fde047_50%)_37.5%_26.67%/20%_25%_no-repeat,linear-gradient(to_bottom_left,transparent_50%,#f97316_50%)_62.5%_26.67%/20%_25%_no-repeat,linear-gradient(to_top_right,transparent_50%,#fb923c_50%)_37.5%_69.23%/20%_35%_no-repeat,linear-gradient(to_top_left,transparent_50%,#facc15_50%)_62.5%_69.23%/20%_35%_no-repeat,linear-gradient(to_bottom_right,transparent_calc(50%_-_1px),rgba(255,255,255,.85)_0_calc(50%_+_1px),transparent_0)_41.86%_100%/14%_20%_no-repeat,linear-gradient(160deg,#0ea5e9,#14b8a6_55%,#84cc16)]" aria-hidden="true"></div>
  <div class="min-w-0 flex-1"><p class="truncate text-[.92rem] font-bold">Paper Kites</p><p class="mt-0.5 truncate text-[.8rem] text-mw-muted">The Lumen Choir</p></div>
  <span class="text-[.78rem] whitespace-nowrap text-mw-muted tabular-nums max-[420px]:hidden"><span data-cur>0:00</span> / <span data-dur>0:24</span></span>
  <button type="button" class="group/m grid size-9 flex-none cursor-pointer place-items-center rounded-full text-mw-muted hover:bg-mw-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mw-accent" data-mute aria-label="Mute" aria-pressed="false"><svg class="block size-[18px]" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 5 6 9H3v6h3l5 4z"/><path class="group-aria-pressed/m:hidden" d="M15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/><path class="hidden group-aria-pressed/m:inline" d="m16 9 6 6m0-6-6 6"/></svg></button>
  <button type="button" class="grid size-10 flex-none cursor-pointer place-items-center rounded-full bg-mw-accent text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mw-accent" data-play="Paper Kites" aria-label="Play Paper Kites"><svg class="block size-[18px] group-data-playing:hidden" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="hidden size-[18px] group-data-playing:block" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
  <div class="absolute inset-x-3.5 -top-[7px]"><input type="range" class="w-full block h-[14px] min-w-0 cursor-pointer appearance-none bg-transparent [--f:var(--p)] focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-[4px] [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,var(--color-mw-accent)_0_calc(7px_+_(100%_-_14px)_*_var(--f)),var(--color-mw-track)_0)] [&::-moz-range-track]:h-[4px] [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-mw-accent)_0_calc(7px_+_(100%_-_14px)_*_var(--f)),var(--color-mw-track)_0)] [&::-webkit-slider-thumb]:-mt-[5px] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-[14px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[color:var(--color-mw-accent)] [&::-webkit-slider-thumb]:border-0 focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-mw-accent)] [&::-moz-range-thumb]:size-[14px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-[color:var(--color-mw-accent)] [&::-moz-range-thumb]:border-0 focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-mw-accent)] [&::-webkit-slider-thumb]:opacity-0 [&::-moz-range-thumb]:opacity-0 [&::-webkit-slider-thumb]:transition-opacity [&::-moz-range-thumb]:transition-opacity hover:[&::-webkit-slider-thumb]:opacity-100 hover:[&::-moz-range-thumb]:opacity-100 focus-visible:[&::-webkit-slider-thumb]:opacity-100 focus-visible:[&::-moz-range-thumb]:opacity-100 motion-reduce:[&::-webkit-slider-thumb]:transition-none motion-reduce:[&::-moz-range-thumb]:transition-none" data-seek min="0" max="24" step="any" value="0" aria-label="Seek Paper Kites" aria-valuetext="0:00 of 0:24"></div>
</div></div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-mw-muted uppercase">Pill player</span>
<div class="group inline-flex max-w-full items-center gap-2.5 rounded-full border border-[#d9d3f0] bg-[linear-gradient(to_right,var(--color-mw-soft)_calc(var(--p)*100%),#fff_0)] py-[5px] pr-4 pl-[5px] text-[#1e1b2e] [--p:0]" data-mw data-length="30" role="group" aria-label="Night Bus to Nowhere by Orla Finch">
  <audio src="../audio/track-3.mp3" preload="metadata"></audio>
  <button type="button" class="grid size-[34px] flex-none cursor-pointer place-items-center rounded-full bg-mw-accent text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mw-accent" data-play="Night Bus to Nowhere" aria-label="Play Night Bus to Nowhere"><svg class="block size-[18px] group-data-playing:hidden" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="hidden size-[18px] group-data-playing:block" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
  <div class="min-w-0 flex-1"><p class="truncate text-[.88rem] font-bold">Night Bus to Nowhere · Orla Finch</p></div>
  <span class="text-[.78rem] whitespace-nowrap text-mw-muted tabular-nums" data-cur>0:00</span>
  <div class="size-[34px] flex-none rounded-full [background:radial-gradient(circle_at_68%_30%,#fef3c7_0_9%,rgba(254,243,199,.16)_9.5%_15%,transparent_15.5%),radial-gradient(circle_at_20%_25%,#fff_0_1.2%,transparent_2%),radial-gradient(circle_at_38%_55%,#fff_0_.9%,transparent_1.6%),radial-gradient(circle_at_86%_64%,#fff_0_1%,transparent_1.7%),radial-gradient(circle_at_12%_70%,#c4b5fd_0_1%,transparent_1.7%),linear-gradient(180deg,#0f172a,#312e81_70%,#7c3aed)]" aria-hidden="true"></div>
</div></div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-mw-muted uppercase">Floating ring widget</span>
<div class="group flex w-[300px] max-w-full items-center gap-3.5 rounded-[20px] bg-white py-3 pr-4 pl-3 text-[#1e1b2e] shadow-[0_2px_6px_rgba(30,27,46,.06),0_18px_40px_rgba(76,29,149,.16)] [--p:0]" data-mw data-length="29" role="group" aria-label="Copper Sky by Milo & the Tidelines">
  <audio src="../audio/track-4.mp3" preload="metadata"></audio>
  <div class="relative size-14 flex-none rounded-full bg-[conic-gradient(var(--color-mw-accent)_calc(var(--p)*1turn),var(--color-mw-track)_0)]">
    <button type="button" class="absolute inset-1 grid cursor-pointer place-items-center rounded-full text-white [background:radial-gradient(circle_at_50%_100%,#fff7ed_0_24%,#fdba74_24%_27%,transparent_27.5%),linear-gradient(180deg,#7c2d12_0_22%,#9a3412_22%_40%,#c2410c_40%_56%,#ea580c_56%_70%,#f59e0b_70%_84%,#fcd34d_84%)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mw-accent" data-play="Copper Sky" aria-label="Play Copper Sky"><svg class="block size-5 drop-shadow-[0_1px_2px_rgba(0,0,0,.5)] group-data-playing:hidden" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg><svg class="hidden size-5 drop-shadow-[0_1px_2px_rgba(0,0,0,.5)] group-data-playing:block" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor"><rect x="6.5" y="5" width="4" height="14" rx="1.2"/><rect x="13.5" y="5" width="4" height="14" rx="1.2"/></svg></button>
  </div>
  <div class="min-w-0 flex-1"><p class="truncate text-[.92rem] font-bold">Copper Sky</p><p class="mt-0.5 truncate text-[.8rem] text-mw-muted">Milo & the Tidelines</p></div>
  <span class="text-[.78rem] whitespace-nowrap text-mw-muted tabular-nums" data-left>0:29 left</span>
</div></div>
CSS
@theme {
  --color-mw-accent: #7c3aed;
  --color-mw-soft: #ede9fe;
  --color-mw-muted: #5b5675;
  --color-mw-track: #8b84a3;
}
JavaScript
// Mini players. Each [data-mw] wraps one <audio>; the script wires whichever controls it contains
// (play button, seek slider, mute button) and writes the elapsed or remaining time.
(() => {
  const fmt = s => { s = Math.floor(s || 0); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
  document.querySelectorAll('[data-mw]').forEach(root => {
    const $ = s => root.querySelector(s);
    const audio = $('audio'), play = $('[data-play]'), seek = $('[data-seek]'), cur = $('[data-cur]'), dur = $('[data-dur]'), left = $('[data-left]');
    const mute = $('[data-mute]'), name = play.dataset.play;
    let raf = 0;
    const length = () => audio.duration || +root.dataset.length;
    const sync = () => {
      const t = audio.currentTime, d = length(), text = fmt(t) + ' of ' + fmt(d);
      root.style.setProperty('--p', d ? t / d : 0);
      if (cur) cur.textContent = fmt(t);
      if (left) left.textContent = fmt(d - t) + ' left';
      if (seek) {
        seek.value = t;
        if (seek.getAttribute('aria-valuetext') !== text) seek.setAttribute('aria-valuetext', text);
      }
    };
    const tick = () => { sync(); raf = requestAnimationFrame(tick); };
    const meta = () => { if (seek) seek.max = audio.duration; if (dur) dur.textContent = fmt(audio.duration); sync(); };
    play.addEventListener('click', () => {
      if (!audio.paused) return audio.pause();
      document.querySelectorAll('audio').forEach(a => { if (a !== audio) a.pause(); });
      audio.play().catch(() => {});
    });
    audio.addEventListener('play', () => { root.setAttribute('data-playing', ''); play.setAttribute('aria-label', 'Pause ' + name); tick(); });
    audio.addEventListener('pause', () => { root.removeAttribute('data-playing'); play.setAttribute('aria-label', 'Play ' + name); cancelAnimationFrame(raf); sync(); });
    audio.addEventListener('loadedmetadata', meta);
    if (audio.readyState > 0) meta();
    if (seek) seek.addEventListener('input', () => { audio.currentTime = seek.value; sync(); });
    // Arrow keys jump 5 seconds; Home and End work natively.
    if (seek) seek.addEventListener('keydown', e => {
      const step = { ArrowLeft: -5, ArrowDown: -5, ArrowRight: 5, ArrowUp: 5 }[e.key];
      if (!step) return;
      e.preventDefault();
      audio.currentTime = Math.min(Math.max(audio.currentTime + step, 0), length());
      sync();
    });
    if (mute) {
      mute.addEventListener('click', () => { audio.muted = !audio.muted; });
      audio.addEventListener('volumechange', () => mute.setAttribute('aria-pressed', String(audio.muted)));
    }
  });
})();

About this set

Three small players for when the music should stay out of the way. The sticky mini player bar uses position: sticky with bottom: 0, so it docks to the bottom of the viewport while the page scrolls; it shows a cover, title and artist, the elapsed and total time, a mute button and a play button, and a 4 pixel progress line along its top edge is a real range input whose thumb appears on hover and keyboard focus. Below 420 pixels the times hide. The pill player is a single rounded line with a play button, the title and artist, the elapsed time and a round cover, and its background fills from the left as the track plays; in Bootstrap that fill is a .progress element behind the content. The floating ring widget wraps a cover-art play button in a 56 pixel conic-gradient ring that closes as the track plays, with the title, artist and time left beside it; give it position: fixed with a right and bottom offset to float it in a corner. The script writes one –p custom property from 0 to 1 that drives all three progress displays.

What’s included

  • Sticky bar that docks to the bottom of the viewport
  • Thin seek line whose thumb appears on hover and focus
  • Pill whose background fills as the track plays
  • Conic-gradient progress ring around a cover-art play button
  • Remaining time written as 0:16 left
  • Bootstrap version uses .card, .rounded-pill, .progress and .form-range

Accessibility

Each widget is a group named after the track. The play button is labelled Play or Pause with the track name, the progress line in the sticky bar is a range input with an aria-label and aria-valuetext, and the mute button uses aria-pressed. The pill fill and the ring are decorative because the elapsed or remaining time is shown as text. Keyboard focus draws a 2px outline on buttons and reveals the slider thumb with a ring; the thumb fade is removed under prefers-reduced-motion.

Customise it

Change --mw-accent, --mw-soft and --mw-track on .mw; --mw-soft is the pill fill and --mw-track the empty part of the line and ring. In Tailwind edit the --color-mw-* theme colours, and the size- utilities on the ring and buttons.