Open demo

CSS code

HTML
<div><span class="lbl">Full bar with volume</span>
<div class="ap ap-bar" data-ap role="group" aria-label="Harbour Lights by Juniper Vale">
  <audio src="../audio/track-1.mp3" preload="metadata"></audio>
  <button type="button" class="ap-play" data-play="Harbour Lights" aria-label="Play Harbour Lights">
    <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>
  <span class="ap-time" data-cur>0:00</span>
  <input type="range" class="ap-seek" data-seek min="0" max="24" step="any" value="0" aria-label="Seek Harbour Lights" aria-valuetext="0:00 of 0:24">
  <span class="ap-time" data-dur>0:24</span>
  <button type="button" class="ap-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>
  <input type="range" class="ap-vol" data-vol min="0" max="1" step="0.05" value="0.8" aria-label="Volume" aria-valuetext="80%">
</div></div>

<div><span class="lbl">Bar with track info</span>
<div class="ap ap-info" data-ap role="group" aria-label="Paper Kites by The Lumen Choir">
  <audio src="../audio/track-2.mp3" preload="metadata"></audio>
  <div class="ap-art" aria-hidden="true"></div>
  <div class="ap-meta"><p class="ap-title">Paper Kites</p><p class="ap-artist">The Lumen Choir</p></div>
  <button type="button" class="ap-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="ap-row">
    <span class="ap-time" data-cur>0:00</span>
    <input type="range" class="ap-seek" data-seek min="0" max="24" step="any" value="0" aria-label="Seek Paper Kites" aria-valuetext="0:00 of 0:24">
    <span class="ap-time" data-dur>0:24</span>
  </div>
</div></div>

<div><span class="lbl">Voice-note bubble</span>
<div class="ap ap-note" data-ap role="group" aria-label="Voice note from Milo">
  <audio src="../audio/track-4.mp3" preload="metadata"></audio>
  <button type="button" class="ap-play" data-play="voice note" aria-label="Play voice note">
    <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>
  <input type="range" class="ap-seek" data-seek min="0" max="29" step="any" value="0" aria-label="Seek voice note" aria-valuetext="0:00 of 0:29">
  <span class="ap-time" data-dur>0:29</span>
</div></div>
CSS
/* Compact audio player bars: a native <audio> with no controls, driven by real buttons and range inputs.
   The script sets --p (playback, 0 to 1) and --v (volume, 0 to 1) on .ap and toggles [data-playing]. */
.ap{--ap-accent:#4f46e5;--ap-ink:#1e293b;--ap-muted:#56657a;--ap-track:#8391a7;--ap-thumb:14px;--ap-h:4px;--p:0;--v:1;
  display:flex;align-items:center;gap:12px;color:var(--ap-ink)}
.ap button{flex:none;display:grid;place-items:center;border:0;background:none;color:inherit;cursor:pointer;font:inherit}
.ap button:focus-visible{outline:2px solid var(--ap-accent);outline-offset:2px}
.ap svg{display:block;width:20px;height:20px}
.ap .ap-play{width:40px;height:40px;border-radius:50%;background:var(--ap-accent);color:#fff;
  box-shadow:0 4px 12px rgba(79,70,229,.35);transition:transform .15s}
.ap .ap-play:hover{transform:scale(1.06)}
.ap .i-pause,.ap[data-playing] .i-play{display:none}
.ap[data-playing] .i-pause{display:block}
.ap-time{font-size:.8rem;font-variant-numeric:tabular-nums;color:var(--ap-muted);min-width:2.2em}
/* range inputs: --f is the filled share of the track */
.ap input[type="range"]{-webkit-appearance:none;appearance:none;display:block;flex:1;min-width:0;height:var(--ap-thumb);
  margin:0;background:none;cursor:pointer;--f:var(--p)}
.ap input[type="range"].ap-vol{--f:var(--v);flex:none;width:84px}
.ap input[type="range"]:focus-visible{outline:none}
.ap input[type="range"]::-webkit-slider-runnable-track{height:var(--ap-h);border-radius:999px;
  background:linear-gradient(to right,var(--ap-accent) 0 calc(var(--ap-thumb) / 2 + (100% - var(--ap-thumb)) * var(--f)),var(--ap-track) 0)}
.ap input[type="range"]::-moz-range-track{height:var(--ap-h);border-radius:999px;
  background:linear-gradient(to right,var(--ap-accent) 0 calc(var(--ap-thumb) / 2 + (100% - var(--ap-thumb)) * var(--f)),var(--ap-track) 0)}
.ap input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:var(--ap-thumb);height:var(--ap-thumb);
  margin-top:calc((var(--ap-h) - var(--ap-thumb)) / 2);border:0;border-radius:50%;background:var(--ap-accent);box-shadow:0 1px 3px rgba(15,23,42,.3)}
.ap input[type="range"]::-moz-range-thumb{width:var(--ap-thumb);height:var(--ap-thumb);border:0;border-radius:50%;
  background:var(--ap-accent);box-shadow:0 1px 3px rgba(15,23,42,.3)}
.ap input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--ap-accent)}
.ap input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--ap-accent)}
.ap .ap-mute{width:34px;height:34px;border-radius:10px;color:var(--ap-muted)}
.ap .ap-mute:hover{background:#eef2f7}
.ap-mute .i-x,.ap-mute[aria-pressed="true"] .i-wave{display:none}
.ap-mute[aria-pressed="true"] .i-x{display:block}
/* 1. full bar */
.ap-bar{background:#fff;border:1px solid #dfe5ee;border-radius:999px;padding:8px 18px 8px 8px;box-shadow:0 6px 20px rgba(15,23,42,.06)}
/* 2. bar with track info */
.ap-info{display:grid;grid-template-columns:auto 1fr auto;gap:10px 14px;background:#fff;border:1px solid #dfe5ee;
  border-radius:18px;padding:14px 16px;box-shadow:0 6px 20px rgba(15,23,42,.06)}
.ap-art{width:48px;height:48px;border-radius:12px;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)}
.ap-meta{min-width:0;align-self:center}
.ap-title{font-weight:700;font-size:.98rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ap-artist{font-size:.85rem;color:var(--ap-muted);margin-top:2px}
.ap-info .ap-play{align-self:center}
.ap-row{grid-column:1 / -1;display:flex;align-items:center;gap:10px}
.ap-row .ap-time:last-child{text-align:right}
/* 3. voice-note bubble */
.ap-note{--ap-accent:#fff;--ap-track:rgba(255,255,255,.55);--ap-ink:#fff;--ap-muted:#e0e7ff;
  max-width:340px;background:#4338ca;border-radius:22px 22px 6px 22px;padding:10px 16px 10px 10px;margin-left:auto}
.ap-note .ap-play{background:#fff;color:#4338ca;box-shadow:none}
.ap-note button:focus-visible{outline-color:#fff}
.ap-note input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #4338ca,0 0 0 5px #fff}
.ap-note input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #4338ca,0 0 0 5px #fff}
@media (max-width:480px){.ap input[type="range"].ap-vol{display:none}.ap-bar{padding-right:10px}}
@media (prefers-reduced-motion:reduce){.ap .ap-play{transition:none}.ap .ap-play:hover{transform:none}}
JavaScript
// Each [data-ap] wraps one <audio>. The script wires its play button, seek slider, mute button and volume slider.
(() => {
  const fmt = s => { s = Math.floor(s || 0); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
  document.querySelectorAll('[data-ap]').forEach(root => {
    const $ = s => root.querySelector(s);
    const audio = $('audio'), play = $('[data-play]'), seek = $('[data-seek]'), cur = $('[data-cur]'), dur = $('[data-dur]');
    const vol = $('[data-vol]'), mute = $('[data-mute]'), name = play.dataset.play;
    let raf = 0;
    const length = () => audio.duration || +seek.max;
    const sync = () => {
      const t = audio.currentTime, d = length(), text = fmt(t) + ' of ' + fmt(d);
      seek.value = t;
      root.style.setProperty('--p', d ? t / d : 0);
      if (cur) cur.textContent = fmt(t);
      if (seek.getAttribute('aria-valuetext') !== text) seek.setAttribute('aria-valuetext', text);
    };
    const tick = () => { sync(); raf = requestAnimationFrame(tick); };
    const meta = () => { seek.max = audio.duration; if (dur) dur.textContent = fmt(audio.duration); sync(); };
    const sound = () => {
      const v = audio.muted ? 0 : audio.volume;
      root.style.setProperty('--v', v);
      if (vol) { vol.value = v; vol.setAttribute('aria-valuetext', Math.round(v * 100) + '%'); }
      if (mute) mute.setAttribute('aria-pressed', String(v === 0));
    };
    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);
    audio.addEventListener('volumechange', sound);
    if (audio.readyState > 0) meta();
    seek.addEventListener('input', () => { audio.currentTime = seek.value; sync(); });
    // Arrow keys jump 5 seconds; Home and End work natively.
    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 (vol) {
      audio.volume = vol.value;
      vol.addEventListener('input', () => { audio.volume = vol.value; audio.muted = false; });
    }
    if (mute) mute.addEventListener('click', () => {
      if (audio.muted || audio.volume === 0) { audio.muted = false; if (audio.volume === 0) audio.volume = 0.6; }
      else audio.muted = true;
    });
    sound();
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Full bar with volume</span>
<div class="ap ap-bar d-flex align-items-center bg-white rounded-pill" data-ap role="group" aria-label="Harbour Lights by Juniper Vale">
  <audio src="../audio/track-1.mp3" preload="metadata"></audio>
  <button type="button" class="btn ap-play rounded-circle" data-play="Harbour Lights" aria-label="Play Harbour Lights">
    <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>
  <span class="ap-time" data-cur>0:00</span>
  <input type="range" class="form-range ap-seek" data-seek min="0" max="24" step="any" value="0" aria-label="Seek Harbour Lights" aria-valuetext="0:00 of 0:24">
  <span class="ap-time" data-dur>0:24</span>
  <button type="button" class="btn ap-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>
  <input type="range" class="form-range ap-vol" data-vol min="0" max="1" step="0.05" value="0.8" aria-label="Volume" aria-valuetext="80%">
</div></div>

<div><span class="lbl">Bar with track info</span>
<div class="ap ap-info card" data-ap 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="ap-art" aria-hidden="true"></div>
  <div class="align-self-center text-truncate"><p class="ap-title mb-0 text-truncate">Paper Kites</p><p class="ap-artist mb-0">The Lumen Choir</p></div>
  <button type="button" class="btn ap-play rounded-circle align-self-center" 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="ap-row d-flex align-items-center">
    <span class="ap-time" data-cur>0:00</span>
    <input type="range" class="form-range ap-seek" data-seek min="0" max="24" step="any" value="0" aria-label="Seek Paper Kites" aria-valuetext="0:00 of 0:24">
    <span class="ap-time text-end" data-dur>0:24</span>
  </div>
  </div>
</div></div>

<div><span class="lbl">Voice-note bubble</span>
<div class="ap ap-note d-flex align-items-center ms-auto" data-ap role="group" aria-label="Voice note from Milo">
  <audio src="../audio/track-4.mp3" preload="metadata"></audio>
  <button type="button" class="btn ap-play rounded-circle" data-play="voice note" aria-label="Play voice note">
    <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>
  <input type="range" class="form-range ap-seek" data-seek min="0" max="29" step="any" value="0" aria-label="Seek voice note" aria-valuetext="0:00 of 0:29">
  <span class="ap-time" data-dur>0:29</span>
</div></div>
CSS
/* Compact audio player bars on Bootstrap: .btn buttons and .form-range sliders around a native <audio>.
   The script sets --p (playback, 0 to 1) and --v (volume, 0 to 1) on .ap and toggles [data-playing]. */
.ap{--ap-accent:#4f46e5;--ap-ink:#1e293b;--ap-muted:#56657a;--ap-track:#8391a7;--ap-thumb:14px;--ap-h:4px;--p:0;--v:1;gap:12px;color:var(--ap-ink)}
.ap .btn{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-border-width:0;display:grid;place-items:center;flex:none}
.ap .btn:focus-visible{outline:2px solid var(--ap-accent);outline-offset:2px;box-shadow:none}
.ap svg{display:block;width:20px;height:20px}
.ap .ap-play{--bs-btn-bg:var(--ap-accent);--bs-btn-color:#fff;--bs-btn-hover-bg:#4338ca;--bs-btn-hover-color:#fff;
  --bs-btn-active-bg:#3730a3;--bs-btn-active-color:#fff;width:40px;height:40px;box-shadow:0 4px 12px rgba(79,70,229,.35);transition:transform .15s}
.ap .ap-play:hover{transform:scale(1.06)}
.ap .i-pause,.ap[data-playing] .i-play{display:none}
.ap[data-playing] .i-pause{display:block}
.ap-time{font-size:.8rem;font-variant-numeric:tabular-nums;color:var(--ap-muted);min-width:2.2em}
.ap .form-range{flex:1;min-width:0;width:auto;height:var(--ap-thumb);--f:var(--p)}
.ap .form-range.ap-vol{--f:var(--v);flex:none;width:84px}
.ap .form-range::-webkit-slider-runnable-track{height:var(--ap-h);border-radius:999px;
  background:linear-gradient(to right,var(--ap-accent) 0 calc(var(--ap-thumb) / 2 + (100% - var(--ap-thumb)) * var(--f)),var(--ap-track) 0)}
.ap .form-range::-moz-range-track{height:var(--ap-h);border-radius:999px;
  background:linear-gradient(to right,var(--ap-accent) 0 calc(var(--ap-thumb) / 2 + (100% - var(--ap-thumb)) * var(--f)),var(--ap-track) 0)}
.ap .form-range::-webkit-slider-thumb{width:var(--ap-thumb);height:var(--ap-thumb);margin-top:calc((var(--ap-h) - var(--ap-thumb)) / 2);
  background:var(--ap-accent);box-shadow:0 1px 3px rgba(15,23,42,.3)}
.ap .form-range::-moz-range-thumb{width:var(--ap-thumb);height:var(--ap-thumb);background:var(--ap-accent);box-shadow:0 1px 3px rgba(15,23,42,.3)}
.ap .form-range:focus::-webkit-slider-thumb{box-shadow:0 1px 3px rgba(15,23,42,.3)}
.ap .form-range:focus::-moz-range-thumb{box-shadow:0 1px 3px rgba(15,23,42,.3)}
.ap .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--ap-accent)}
.ap .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--ap-accent)}
.ap .ap-mute{--bs-btn-color:var(--ap-muted);--bs-btn-hover-bg:#eef2f7;--bs-btn-hover-color:var(--ap-muted);--bs-btn-active-bg:#e2e8f0;
  --bs-btn-border-radius:10px;width:34px;height:34px}
.ap-mute .i-x,.ap-mute[aria-pressed="true"] .i-wave{display:none}
.ap-mute[aria-pressed="true"] .i-x{display:block}
/* 1. full bar */
.ap-bar{border:1px solid #dfe5ee;padding:8px 18px 8px 8px;box-shadow:0 6px 20px rgba(15,23,42,.06)}
/* 2. bar with track info */
.ap-info{border-color:#dfe5ee;border-radius:18px;box-shadow:0 6px 20px rgba(15,23,42,.06)}
.ap-info .card-body{display:grid;grid-template-columns:auto 1fr auto;gap:10px 14px;padding:14px 16px}
.ap-art{width:48px;height:48px;border-radius:12px;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)}
.ap-title{font-weight:700;font-size:.98rem}
.ap-artist{font-size:.85rem;color:var(--ap-muted);margin-top:2px}
.ap-row{grid-column:1 / -1;gap:10px}
/* 3. voice-note bubble */
.ap-note{--ap-accent:#fff;--ap-track:rgba(255,255,255,.55);--ap-ink:#fff;--ap-muted:#e0e7ff;
  max-width:340px;background:#4338ca;border-radius:22px 22px 6px 22px;padding:10px 16px 10px 10px}
.ap-note .ap-play{--bs-btn-bg:#fff;--bs-btn-color:#4338ca;--bs-btn-hover-bg:#e0e7ff;--bs-btn-hover-color:#4338ca;
  --bs-btn-active-bg:#c7d2fe;--bs-btn-active-color:#4338ca;box-shadow:none}
.ap-note .btn:focus-visible{outline-color:#fff}
.ap-note .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #4338ca,0 0 0 5px #fff}
.ap-note .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #4338ca,0 0 0 5px #fff}
@media (max-width:480px){.ap .form-range.ap-vol{display:none}.ap-bar{padding-right:10px}}
@media (prefers-reduced-motion:reduce){.ap .ap-play{transition:none}.ap .ap-play:hover{transform:none}}
JavaScript
// Each [data-ap] wraps one <audio>. The script wires its play button, seek slider, mute button and volume slider.
(() => {
  const fmt = s => { s = Math.floor(s || 0); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
  document.querySelectorAll('[data-ap]').forEach(root => {
    const $ = s => root.querySelector(s);
    const audio = $('audio'), play = $('[data-play]'), seek = $('[data-seek]'), cur = $('[data-cur]'), dur = $('[data-dur]');
    const vol = $('[data-vol]'), mute = $('[data-mute]'), name = play.dataset.play;
    let raf = 0;
    const length = () => audio.duration || +seek.max;
    const sync = () => {
      const t = audio.currentTime, d = length(), text = fmt(t) + ' of ' + fmt(d);
      seek.value = t;
      root.style.setProperty('--p', d ? t / d : 0);
      if (cur) cur.textContent = fmt(t);
      if (seek.getAttribute('aria-valuetext') !== text) seek.setAttribute('aria-valuetext', text);
    };
    const tick = () => { sync(); raf = requestAnimationFrame(tick); };
    const meta = () => { seek.max = audio.duration; if (dur) dur.textContent = fmt(audio.duration); sync(); };
    const sound = () => {
      const v = audio.muted ? 0 : audio.volume;
      root.style.setProperty('--v', v);
      if (vol) { vol.value = v; vol.setAttribute('aria-valuetext', Math.round(v * 100) + '%'); }
      if (mute) mute.setAttribute('aria-pressed', String(v === 0));
    };
    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);
    audio.addEventListener('volumechange', sound);
    if (audio.readyState > 0) meta();
    seek.addEventListener('input', () => { audio.currentTime = seek.value; sync(); });
    // Arrow keys jump 5 seconds; Home and End work natively.
    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 (vol) {
      audio.volume = vol.value;
      vol.addEventListener('input', () => { audio.volume = vol.value; audio.muted = false; });
    }
    if (mute) mute.addEventListener('click', () => {
      if (audio.muted || audio.volume === 0) { audio.muted = false; if (audio.volume === 0) audio.volume = 0.6; }
      else audio.muted = true;
    });
    sound();
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#56657a] uppercase">Full bar with volume</span>
<div class="group flex items-center gap-3 rounded-full border border-[#dfe5ee] bg-white py-2 pr-[18px] pl-2 text-[#1e293b] shadow-[0_6px_20px_rgba(15,23,42,.06)] [--p:0] [--v:1] max-[480px]:pr-2.5" data-ap role="group" aria-label="Harbour Lights by Juniper Vale">
  <audio src="../audio/track-1.mp3" preload="metadata"></audio>
  <button type="button" class="grid size-10 flex-none cursor-pointer place-items-center rounded-full bg-ap-accent text-white shadow-[0_4px_12px_rgba(79,70,229,.35)] transition-transform duration-150 hover:scale-106 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ap-accent motion-reduce:transition-none motion-reduce:hover:scale-100" data-play="Harbour Lights" aria-label="Play Harbour Lights">
    <svg class="block size-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 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>
  <span class="min-w-[2.2em] text-[.8rem] text-ap-muted tabular-nums" data-cur>0:00</span>
  <input type="range" class="flex-1 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-ap-accent)_0_calc(7px_+_(100%_-_14px)_*_var(--f)),var(--color-ap-track)_0)] [&::-moz-range-track]:h-[4px] [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-ap-accent)_0_calc(7px_+_(100%_-_14px)_*_var(--f)),var(--color-ap-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-ap-accent)] [&::-webkit-slider-thumb]:border-0 [&::-webkit-slider-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-ap-accent)] [&::-moz-range-thumb]:size-[14px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-[color:var(--color-ap-accent)] [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.3)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-ap-accent)]" data-seek min="0" max="24" step="any" value="0" aria-label="Seek Harbour Lights" aria-valuetext="0:00 of 0:24">
  <span class="min-w-[2.2em] text-[.8rem] text-ap-muted tabular-nums" data-dur>0:24</span>
  <button type="button" class="group/m grid size-[34px] flex-none cursor-pointer place-items-center rounded-[10px] text-ap-muted hover:bg-[#eef2f7] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ap-accent" data-mute aria-label="Mute" aria-pressed="false">
    <svg class="block size-5" 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>
  <input type="range" class="w-[84px] flex-none max-[480px]:hidden block h-[14px] min-w-0 cursor-pointer appearance-none bg-transparent [--f:var(--v)] 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-ap-accent)_0_calc(7px_+_(100%_-_14px)_*_var(--f)),var(--color-ap-track)_0)] [&::-moz-range-track]:h-[4px] [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-ap-accent)_0_calc(7px_+_(100%_-_14px)_*_var(--f)),var(--color-ap-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-ap-accent)] [&::-webkit-slider-thumb]:border-0 [&::-webkit-slider-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-ap-accent)] [&::-moz-range-thumb]:size-[14px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-[color:var(--color-ap-accent)] [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.3)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-ap-accent)]" data-vol min="0" max="1" step="0.05" value="0.8" aria-label="Volume" aria-valuetext="80%">
</div></div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#56657a] uppercase">Bar with track info</span>
<div class="group grid grid-cols-[auto_1fr_auto] gap-x-3.5 gap-y-2.5 rounded-[18px] border border-[#dfe5ee] bg-white px-4 py-3.5 text-[#1e293b] shadow-[0_6px_20px_rgba(15,23,42,.06)] [--p:0]" data-ap role="group" aria-label="Paper Kites by The Lumen Choir">
  <audio src="../audio/track-2.mp3" preload="metadata"></audio>
  <div class="size-12 rounded-xl [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 self-center"><p class="truncate text-[.98rem] font-bold">Paper Kites</p><p class="mt-0.5 text-[.85rem] text-ap-muted">The Lumen Choir</p></div>
  <button type="button" class="grid size-10 flex-none cursor-pointer place-items-center self-center rounded-full bg-ap-accent text-white shadow-[0_4px_12px_rgba(79,70,229,.35)] transition-transform duration-150 hover:scale-106 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ap-accent motion-reduce:transition-none motion-reduce:hover:scale-100" data-play="Paper Kites" aria-label="Play Paper Kites">
    <svg class="block size-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 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="col-span-full flex items-center gap-2.5">
    <span class="min-w-[2.2em] text-[.8rem] text-ap-muted tabular-nums" data-cur>0:00</span>
    <input type="range" class="flex-1 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-ap-accent)_0_calc(7px_+_(100%_-_14px)_*_var(--f)),var(--color-ap-track)_0)] [&::-moz-range-track]:h-[4px] [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-ap-accent)_0_calc(7px_+_(100%_-_14px)_*_var(--f)),var(--color-ap-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-ap-accent)] [&::-webkit-slider-thumb]:border-0 [&::-webkit-slider-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-ap-accent)] [&::-moz-range-thumb]:size-[14px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-[color:var(--color-ap-accent)] [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.3)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-ap-accent)]" data-seek min="0" max="24" step="any" value="0" aria-label="Seek Paper Kites" aria-valuetext="0:00 of 0:24">
    <span class="min-w-[2.2em] text-right text-[.8rem] text-ap-muted tabular-nums" data-dur>0:24</span>
  </div>
</div></div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#56657a] uppercase">Voice-note bubble</span>
<div class="group ml-auto flex max-w-[340px] items-center gap-3 rounded-[22px_22px_6px_22px] bg-[#4338ca] py-2.5 pr-4 pl-2.5 text-white [--p:0]" data-ap role="group" aria-label="Voice note from Milo">
  <audio src="../audio/track-4.mp3" preload="metadata"></audio>
  <button type="button" class="grid size-10 flex-none cursor-pointer place-items-center rounded-full bg-white text-[#4338ca] transition-transform duration-150 hover:scale-106 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white motion-reduce:transition-none motion-reduce:hover:scale-100" data-play="voice note" aria-label="Play voice note">
    <svg class="block size-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 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>
  <input type="range" class="flex-1 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,#fff_0_calc(7px_+_(100%_-_14px)_*_var(--f)),rgba(255,255,255,.55)_0)] [&::-moz-range-track]:h-[4px] [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,#fff_0_calc(7px_+_(100%_-_14px)_*_var(--f)),rgba(255,255,255,.55)_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-[#fff] [&::-webkit-slider-thumb]:border-0 [&::-webkit-slider-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#4338ca,0_0_0_5px_#fff] [&::-moz-range-thumb]:size-[14px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-[#fff] [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.3)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#4338ca,0_0_0_5px_#fff]" data-seek min="0" max="29" step="any" value="0" aria-label="Seek voice note" aria-valuetext="0:00 of 0:29">
  <span class="min-w-[2.2em] text-[.8rem] text-[#e0e7ff] tabular-nums" data-dur>0:29</span>
</div></div>
CSS
@theme {
  --color-ap-accent: #4f46e5;
  --color-ap-track: #8391a7;
  --color-ap-muted: #56657a;
}
JavaScript
// Each [data-ap] wraps one <audio>. The script wires its play button, seek slider, mute button and volume slider.
(() => {
  const fmt = s => { s = Math.floor(s || 0); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
  document.querySelectorAll('[data-ap]').forEach(root => {
    const $ = s => root.querySelector(s);
    const audio = $('audio'), play = $('[data-play]'), seek = $('[data-seek]'), cur = $('[data-cur]'), dur = $('[data-dur]');
    const vol = $('[data-vol]'), mute = $('[data-mute]'), name = play.dataset.play;
    let raf = 0;
    const length = () => audio.duration || +seek.max;
    const sync = () => {
      const t = audio.currentTime, d = length(), text = fmt(t) + ' of ' + fmt(d);
      seek.value = t;
      root.style.setProperty('--p', d ? t / d : 0);
      if (cur) cur.textContent = fmt(t);
      if (seek.getAttribute('aria-valuetext') !== text) seek.setAttribute('aria-valuetext', text);
    };
    const tick = () => { sync(); raf = requestAnimationFrame(tick); };
    const meta = () => { seek.max = audio.duration; if (dur) dur.textContent = fmt(audio.duration); sync(); };
    const sound = () => {
      const v = audio.muted ? 0 : audio.volume;
      root.style.setProperty('--v', v);
      if (vol) { vol.value = v; vol.setAttribute('aria-valuetext', Math.round(v * 100) + '%'); }
      if (mute) mute.setAttribute('aria-pressed', String(v === 0));
    };
    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);
    audio.addEventListener('volumechange', sound);
    if (audio.readyState > 0) meta();
    seek.addEventListener('input', () => { audio.currentTime = seek.value; sync(); });
    // Arrow keys jump 5 seconds; Home and End work natively.
    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 (vol) {
      audio.volume = vol.value;
      vol.addEventListener('input', () => { audio.volume = vol.value; audio.muted = false; });
    }
    if (mute) mute.addEventListener('click', () => {
      if (audio.muted || audio.volume === 0) { audio.muted = false; if (audio.volume === 0) audio.volume = 0.6; }
      else audio.muted = true;
    });
    sound();
  });
})();

About this set

Three compact audio players for pages where the sound is one element among many. The full bar with volume puts a round play button, the elapsed time, a seek slider, the total time, a mute button and an 84 pixel volume slider in one white pill; below 480 pixels the volume slider hides and the mute button stays. The bar with track info adds a cover tile, title and artist above a full-width seek row, which suits a single track in an article or a product page. The voice-note bubble is a chat-style indigo bubble with a white play button and a white track, for messages and comment threads. Each player wraps a native audio element with no controls attribute and preload set to metadata, so only the length loads until someone presses play. The buttons are real button elements and the sliders are real range inputs. While audio plays, a short script reads currentTime on every animation frame and writes it to the slider and to a –p custom property that paints the filled part of the track. Starting one player pauses any other on the page.

What’s included

  • Full bar with volume, bar with track info and a voice-note bubble
  • Native audio element driven by real buttons and range inputs
  • Seek slider with aria-valuetext such as 0:05 of 0:24
  • Mute button with aria-pressed and a volume slider that reads as a percentage
  • Arrow keys on the seek slider jump 5 seconds
  • Plain CSS, Bootstrap and Tailwind versions of the same design

Accessibility

Each player is a group named after its track, and the play button's label changes between Play Harbour Lights and Pause Harbour Lights. The seek slider has an aria-label and an aria-valuetext such as 0:05 of 0:24, the mute button uses aria-pressed and the volume slider announces a percentage. Buttons show a 2px outline on keyboard focus and slider thumbs get a two-colour ring; the hover scale on the play button is removed under prefers-reduced-motion.

Customise it

Change --ap-accent, --ap-track, --ap-thumb and --ap-h on .ap in the plain CSS and Bootstrap versions; the Bootstrap buttons also take --bs-btn-bg and --bs-btn-hover-bg. In Tailwind edit the --color-ap-accent, --color-ap-track and --color-ap-muted theme colours.