Open demo

CSS code

HTML
<div><span class="lbl">Vertical album card</span>
<div class="ac ac-card" data-ac role="group" aria-label="Copper Sky by Milo & the Tidelines">
  <audio src="../audio/track-4.mp3" preload="metadata"></audio>
  <div class="ac-art" 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%)" aria-hidden="true"></div>
  <div class="ac-head">
    <div><p class="ac-title">Copper Sky</p><p class="ac-artist">Milo & the Tidelines · Long Way West</p></div>
    <button type="button" class="ac-icon" data-like aria-label="Like Copper Sky" 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="M12 20s-7.5-4.6-7.5-10.2A4.2 4.2 0 0 1 12 7.3a4.2 4.2 0 0 1 7.5 2.5C19.5 15.4 12 20 12 20z"/></svg></button>
  </div>
  <input type="range" data-seek min="0" max="29" step="any" value="0" aria-label="Seek Copper Sky" aria-valuetext="0:00 of 0:29">
  <div class="ac-foot">
    <span class="ac-time" data-cur>0:00</span>
    <button type="button" class="ac-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>
    <span class="ac-time" data-dur>0:29</span>
  </div>
</div></div>

<div><span class="lbl">Cover with overlay controls</span>
<div class="ac ac-over" 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)" data-ac role="group" aria-label="Night Bus to Nowhere by Orla Finch">
  <audio src="../audio/track-3.mp3" preload="metadata"></audio>
  <div class="ac-shade">
    <div class="ac-line">
      <div><p class="ac-title">Night Bus to Nowhere</p><p class="ac-artist">Orla Finch · Last Stop Songs</p></div>
      <button type="button" class="ac-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>
    <input type="range" data-seek min="0" max="30" step="any" value="0" aria-label="Seek Night Bus to Nowhere" aria-valuetext="0:00 of 0:30">
    <div class="ac-times"><span class="ac-time" data-cur>0:00</span><span class="ac-time" data-dur>0:30</span></div>
  </div>
</div></div>

<div class="wide"><span class="lbl">Horizontal card</span>
<div class="ac ac-row" data-ac role="group" aria-label="Harbour Lights by Juniper Vale">
  <audio src="../audio/track-1.mp3" preload="metadata"></audio>
  <div class="ac-art" style="--art:radial-gradient(circle at 68% 40%,#fde68a 0 11%,transparent 11.5%),repeating-linear-gradient(180deg,transparent 0 6%,rgba(253,230,138,.3) 6% 7.5%) bottom/100% 42% no-repeat,linear-gradient(180deg,#fb923c,#e11d48 58%,#312e81 58%,#1e1b4b)" aria-hidden="true"></div>
  <div class="ac-body">
    <div><p class="ac-eyebrow">Now playing</p><p class="ac-title">Harbour Lights</p><p class="ac-artist">Juniper Vale · Low Tide Radio</p></div>
    <div class="ac-line">
      <button type="button" class="ac-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="ac-time" data-cur>0:00</span>
      <input type="range" data-seek min="0" max="24" step="any" value="0" aria-label="Seek Harbour Lights" aria-valuetext="0:00 of 0:24">
      <span class="ac-time" data-dur>0:24</span>
      <button type="button" class="ac-icon ac-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>
    </div>
  </div>
</div></div>
CSS
/* Album card players: cover art drawn with CSS gradients, a native <audio> and real buttons and range inputs.
   The script sets --p (0 to 1) on .ac and toggles [data-playing]. */
.ac{--ac-accent:#be123c;--ac-ink:#1c1917;--ac-muted:#57534e;--ac-track:#8a837d;--ac-thumb:12px;--ac-h:4px;--p:0;
  color:var(--ac-ink);background:#fff;border-radius:22px;box-shadow:0 1px 2px rgba(28,25,23,.06),0 12px 32px rgba(28,25,23,.1)}
.ac button{flex:none;display:grid;place-items:center;border:0;background:none;color:inherit;cursor:pointer;font:inherit}
.ac button:focus-visible{outline:2px solid var(--ac-accent);outline-offset:2px}
.ac svg{display:block;width:20px;height:20px}
.ac .ac-play{width:52px;height:52px;border-radius:50%;background:var(--ac-accent);color:#fff;
  box-shadow:0 6px 16px rgba(190,18,60,.32);transition:transform .15s}
.ac .ac-play svg{width:24px;height:24px}
.ac .ac-play:hover{transform:scale(1.05)}
.ac .i-pause,.ac[data-playing] .i-play{display:none}
.ac[data-playing] .i-pause{display:block}
.ac .ac-icon{width:38px;height:38px;border-radius:50%;color:var(--ac-muted)}
.ac .ac-icon:hover{background:#f4f1ec}
.ac .ac-icon[aria-pressed="true"]{color:var(--ac-accent)}
.ac [data-like][aria-pressed="true"] svg{fill:currentColor}
.ac-mute .i-x,.ac-mute[aria-pressed="true"] .i-wave{display:none}
.ac-mute[aria-pressed="true"] .i-x{display:block}
.ac-art{border-radius:16px;background:var(--art)}
.ac-title{font-weight:700;font-size:1.05rem;letter-spacing:-.01em}
.ac-artist{font-size:.86rem;color:var(--ac-muted);margin-top:3px}
.ac-time{font-size:.78rem;font-variant-numeric:tabular-nums;color:var(--ac-muted)}
/* seek slider: --p is the played share of the track */
.ac input[type="range"]{-webkit-appearance:none;appearance:none;display:block;width:100%;min-width:0;height:var(--ac-thumb);
  margin:0;background:none;cursor:pointer}
.ac input[type="range"]:focus-visible{outline:none}
.ac input[type="range"]::-webkit-slider-runnable-track{height:var(--ac-h);border-radius:999px;
  background:linear-gradient(to right,var(--ac-accent) 0 calc(var(--ac-thumb) / 2 + (100% - var(--ac-thumb)) * var(--p)),var(--ac-track) 0)}
.ac input[type="range"]::-moz-range-track{height:var(--ac-h);border-radius:999px;
  background:linear-gradient(to right,var(--ac-accent) 0 calc(var(--ac-thumb) / 2 + (100% - var(--ac-thumb)) * var(--p)),var(--ac-track) 0)}
.ac input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:var(--ac-thumb);height:var(--ac-thumb);
  margin-top:calc((var(--ac-h) - var(--ac-thumb)) / 2);border:0;border-radius:50%;background:var(--ac-accent)}
.ac input[type="range"]::-moz-range-thumb{width:var(--ac-thumb);height:var(--ac-thumb);border:0;border-radius:50%;background:var(--ac-accent)}
.ac input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--ac-accent)}
.ac input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--ac-accent)}
/* 1. vertical card */
.ac-card{max-width:320px;padding:16px 16px 18px}
.ac-card .ac-art{aspect-ratio:1;margin-bottom:16px}
.ac-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:16px}
.ac-foot{display:flex;align-items:center;justify-content:space-between;margin-top:8px}
/* 2. horizontal card */
.ac-row{display:flex;gap:18px;padding:14px;align-items:stretch}
.ac-row .ac-art{flex:none;width:132px;aspect-ratio:1}
.ac-body{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center;gap:16px}
.ac-eyebrow{font-size:.7rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--ac-accent)}
.ac-line{display:flex;align-items:center;gap:16px}
.ac-line input[type="range"]{flex:1}
.ac-row .ac-play{width:44px;height:44px}
.ac-row .ac-play svg{width:20px;height:20px}
/* 3. cover with overlay controls */
.ac-over{position:relative;max-width:320px;aspect-ratio:1;overflow:hidden;color:#fff;background:var(--art)}
.ac-shade{position:absolute;inset:auto 0 0;padding:70px 18px 16px;
  background:linear-gradient(to top,rgba(15,23,42,.92) 30%,rgba(15,23,42,0))}
.ac-over .ac-artist{color:#e2e8f0}
.ac-over .ac-line{margin-bottom:16px}
.ac-over .ac-line > div{flex:1;min-width:0}
.ac-over .ac-play{background:#fff;color:#1c1917;box-shadow:none}
.ac-over{--ac-accent:#fda4af;--ac-track:rgba(255,255,255,.5)}
.ac-over button:focus-visible{outline-color:#fff}
.ac-over input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #0f172a,0 0 0 5px #fff}
.ac-over input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #0f172a,0 0 0 5px #fff}
.ac-over .ac-time{color:#e2e8f0}
.ac-over .ac-times{display:flex;justify-content:space-between;margin-top:4px}
@media (max-width:480px){.ac-row{flex-direction:column}.ac-row .ac-art{width:100%;aspect-ratio:2 / 1}}
@media (prefers-reduced-motion:reduce){.ac .ac-play{transition:none}.ac .ac-play:hover{transform:none}}
JavaScript
// Each [data-ac] card wraps one <audio>. The script wires its play button, seek slider, mute and like buttons.
(() => {
  const fmt = s => { s = Math.floor(s || 0); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
  document.querySelectorAll('[data-ac]').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();
    // Like: a plain toggle button, its state lives in aria-pressed.
    root.querySelectorAll('[data-like]').forEach(b => b.addEventListener('click', () => {
      b.setAttribute('aria-pressed', String(b.getAttribute('aria-pressed') !== 'true'));
    }));
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Vertical album card</span>
<div class="card ac ac-card" data-ac 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="ac-art" 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%)" aria-hidden="true"></div>
  <div class="d-flex align-items-start justify-content-between gap-2 mb-3">
    <div><p class="ac-title mb-0">Copper Sky</p><p class="ac-artist mb-0">Milo & the Tidelines · Long Way West</p></div>
    <button type="button" class="btn ac-icon rounded-circle" data-like aria-label="Like Copper Sky" 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="M12 20s-7.5-4.6-7.5-10.2A4.2 4.2 0 0 1 12 7.3a4.2 4.2 0 0 1 7.5 2.5C19.5 15.4 12 20 12 20z"/></svg></button>
  </div>
  <input type="range" class="form-range" data-seek min="0" max="29" step="any" value="0" aria-label="Seek Copper Sky" aria-valuetext="0:00 of 0:29">
  <div class="d-flex align-items-center justify-content-between mt-2">
    <span class="ac-time" data-cur>0:00</span>
    <button type="button" class="btn ac-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>
    <span class="ac-time" data-dur>0:29</span>
  </div>
  </div>
</div></div>

<div><span class="lbl">Cover with overlay controls</span>
<div class="card ac ac-over" 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)" data-ac role="group" aria-label="Night Bus to Nowhere by Orla Finch">
  <audio src="../audio/track-3.mp3" preload="metadata"></audio>
  <div class="card-img-overlay">
    <div class="d-flex align-items-center gap-3 mb-3">
      <div class="flex-grow-1" style="min-width:0"><p class="ac-title mb-0">Night Bus to Nowhere</p><p class="ac-artist mb-0">Orla Finch · Last Stop Songs</p></div>
      <button type="button" class="btn ac-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>
    <input type="range" class="form-range" data-seek min="0" max="30" step="any" value="0" aria-label="Seek Night Bus to Nowhere" aria-valuetext="0:00 of 0:30">
    <div class="d-flex justify-content-between mt-1"><span class="ac-time" data-cur>0:00</span><span class="ac-time" data-dur>0:30</span></div>
  </div>
</div></div>

<div class="wide"><span class="lbl">Horizontal card</span>
<div class="card ac ac-row" data-ac role="group" aria-label="Harbour Lights by Juniper Vale">
  <audio src="../audio/track-1.mp3" preload="metadata"></audio>
  <div class="ac-art" style="--art:radial-gradient(circle at 68% 40%,#fde68a 0 11%,transparent 11.5%),repeating-linear-gradient(180deg,transparent 0 6%,rgba(253,230,138,.3) 6% 7.5%) bottom/100% 42% no-repeat,linear-gradient(180deg,#fb923c,#e11d48 58%,#312e81 58%,#1e1b4b)" aria-hidden="true"></div>
  <div class="flex-grow-1 d-flex flex-column justify-content-center gap-3" style="min-width:0">
    <div><p class="ac-eyebrow mb-0">Now playing</p><p class="ac-title mb-0">Harbour Lights</p><p class="ac-artist mb-0">Juniper Vale · Low Tide Radio</p></div>
    <div class="d-flex align-items-center gap-3">
      <button type="button" class="btn ac-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="ac-time" data-cur>0:00</span>
      <input type="range" class="form-range flex-grow-1" data-seek min="0" max="24" step="any" value="0" aria-label="Seek Harbour Lights" aria-valuetext="0:00 of 0:24">
      <span class="ac-time" data-dur>0:24</span>
      <button type="button" class="btn ac-icon ac-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>
    </div>
  </div>
</div></div>
CSS
/* Album card players on Bootstrap: .card, .btn and .form-range around a native <audio>.
   The script sets --p (0 to 1) on .ac and toggles [data-playing]. */
.ac{--ac-accent:#be123c;--ac-ink:#1c1917;--ac-muted:#57534e;--ac-track:#8a837d;--ac-thumb:12px;--ac-h:4px;--p:0;
  --bs-card-border-width:0;--bs-card-border-radius:22px;--bs-card-color:var(--ac-ink);color:var(--ac-ink);
  box-shadow:0 1px 2px rgba(28,25,23,.06),0 12px 32px rgba(28,25,23,.1)}
.ac .btn{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-border-width:0;flex:none;display:grid;place-items:center}
.ac .btn:focus-visible{outline:2px solid var(--ac-accent);outline-offset:2px;box-shadow:none}
.ac svg{display:block;width:20px;height:20px}
.ac .ac-play{--bs-btn-bg:var(--ac-accent);--bs-btn-color:#fff;--bs-btn-hover-bg:#9f1239;--bs-btn-hover-color:#fff;
  --bs-btn-active-bg:#881337;--bs-btn-active-color:#fff;width:52px;height:52px;box-shadow:0 6px 16px rgba(190,18,60,.32);transition:transform .15s}
.ac .ac-play svg{width:24px;height:24px}
.ac .ac-play:hover{transform:scale(1.05)}
.ac .i-pause,.ac[data-playing] .i-play{display:none}
.ac[data-playing] .i-pause{display:block}
.ac .ac-icon{--bs-btn-color:var(--ac-muted);--bs-btn-hover-bg:#f4f1ec;--bs-btn-hover-color:var(--ac-muted);--bs-btn-active-bg:#ebe6df;
  --bs-btn-active-color:var(--ac-accent);width:38px;height:38px}
.ac .ac-icon[aria-pressed="true"]{--bs-btn-color:var(--ac-accent);--bs-btn-hover-color:var(--ac-accent)}
.ac [data-like][aria-pressed="true"] svg{fill:currentColor}
.ac-mute .i-x,.ac-mute[aria-pressed="true"] .i-wave{display:none}
.ac-mute[aria-pressed="true"] .i-x{display:block}
.ac-art{border-radius:16px;background:var(--art)}
.ac-title{font-weight:700;font-size:1.05rem;letter-spacing:-.01em}
.ac-artist{font-size:.86rem;color:var(--ac-muted);margin-top:3px}
.ac-time{font-size:.78rem;font-variant-numeric:tabular-nums;color:var(--ac-muted)}
.ac .form-range{display:block;min-width:0;height:var(--ac-thumb)}
.ac .form-range::-webkit-slider-runnable-track{height:var(--ac-h);border-radius:999px;
  background:linear-gradient(to right,var(--ac-accent) 0 calc(var(--ac-thumb) / 2 + (100% - var(--ac-thumb)) * var(--p)),var(--ac-track) 0)}
.ac .form-range::-moz-range-track{height:var(--ac-h);border-radius:999px;
  background:linear-gradient(to right,var(--ac-accent) 0 calc(var(--ac-thumb) / 2 + (100% - var(--ac-thumb)) * var(--p)),var(--ac-track) 0)}
.ac .form-range::-webkit-slider-thumb{width:var(--ac-thumb);height:var(--ac-thumb);margin-top:calc((var(--ac-h) - var(--ac-thumb)) / 2);background:var(--ac-accent)}
.ac .form-range::-moz-range-thumb{width:var(--ac-thumb);height:var(--ac-thumb);background:var(--ac-accent)}
.ac .form-range:focus::-webkit-slider-thumb{box-shadow:none}
.ac .form-range:focus::-moz-range-thumb{box-shadow:none}
.ac .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--ac-accent)}
.ac .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--ac-accent)}
/* 1. vertical card */
.ac-card{max-width:320px}
.ac-card .card-body{padding:16px 16px 18px}
.ac-card .ac-art{aspect-ratio:1;margin-bottom:16px}
/* 2. horizontal card */
.ac-row{flex-direction:row;gap:18px;padding:14px}
.ac-row .ac-art{flex:none;width:132px;aspect-ratio:1}
.ac-eyebrow{font-size:.7rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--ac-accent)}
.ac-row .ac-play{width:44px;height:44px}
.ac-row .ac-play svg{width:20px;height:20px}
/* 3. cover with overlay controls */
.ac-over{max-width:320px;aspect-ratio:1;overflow:hidden;--bs-card-color:#fff;color:#fff;background:var(--art);--ac-accent:#fda4af;--ac-track:rgba(255,255,255,.5)}
.ac-over .card-img-overlay{top:auto;padding:70px 18px 16px;border-radius:0;background:linear-gradient(to top,rgba(15,23,42,.92) 30%,rgba(15,23,42,0))}
.ac-over .ac-artist,.ac-over .ac-time{color:#e2e8f0}
.ac-over .ac-play{--bs-btn-bg:#fff;--bs-btn-color:#1c1917;--bs-btn-hover-bg:#f1f5f9;--bs-btn-hover-color:#1c1917;--bs-btn-active-bg:#e2e8f0;--bs-btn-active-color:#1c1917;box-shadow:none}
.ac-over .btn:focus-visible{outline-color:#fff}
.ac-over .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #0f172a,0 0 0 5px #fff}
.ac-over .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #0f172a,0 0 0 5px #fff}
@media (max-width:480px){.ac-row{flex-direction:column}.ac-row .ac-art{width:100%;aspect-ratio:2 / 1}}
@media (prefers-reduced-motion:reduce){.ac .ac-play{transition:none}.ac .ac-play:hover{transform:none}}
JavaScript
// Each [data-ac] card wraps one <audio>. The script wires its play button, seek slider, mute and like buttons.
(() => {
  const fmt = s => { s = Math.floor(s || 0); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
  document.querySelectorAll('[data-ac]').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();
    // Like: a plain toggle button, its state lives in aria-pressed.
    root.querySelectorAll('[data-like]').forEach(b => b.addEventListener('click', () => {
      b.setAttribute('aria-pressed', String(b.getAttribute('aria-pressed') !== 'true'));
    }));
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#6b6460] uppercase">Vertical album card</span>
<div class="group max-w-[320px] rounded-[22px] bg-white px-4 pt-4 pb-[18px] text-[#1c1917] shadow-[0_1px_2px_rgba(28,25,23,.06),0_12px_32px_rgba(28,25,23,.1)] [--p:0]" data-ac role="group" aria-label="Copper Sky by Milo & the Tidelines">
  <audio src="../audio/track-4.mp3" preload="metadata"></audio>
  <div class="mb-4 aspect-square rounded-2xl [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%)]" aria-hidden="true"></div>
  <div class="mb-4 flex items-start justify-between gap-2">
    <div><p class="text-[1.05rem] font-bold tracking-[-.01em]">Copper Sky</p><p class="mt-[3px] text-[.86rem] text-ac-muted">Milo & the Tidelines · Long Way West</p></div>
    <button type="button" class="grid size-[38px] flex-none cursor-pointer place-items-center rounded-full text-ac-muted hover:bg-[#f4f1ec] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ac-accent aria-pressed:text-ac-accent aria-pressed:[&_svg]:fill-current" data-like aria-label="Like Copper Sky" 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="M12 20s-7.5-4.6-7.5-10.2A4.2 4.2 0 0 1 12 7.3a4.2 4.2 0 0 1 7.5 2.5C19.5 15.4 12 20 12 20z"/></svg></button>
  </div>
  <input type="range" class="w-full block h-[12px] 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-ac-accent)_0_calc(6px_+_(100%_-_12px)_*_var(--f)),var(--color-ac-track)_0)] [&::-moz-range-track]:h-[4px] [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-ac-accent)_0_calc(6px_+_(100%_-_12px)_*_var(--f)),var(--color-ac-track)_0)] [&::-webkit-slider-thumb]:-mt-[4px] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-[12px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[color:var(--color-ac-accent)] [&::-webkit-slider-thumb]:border-0 focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-ac-accent)] [&::-moz-range-thumb]:size-[12px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-[color:var(--color-ac-accent)] [&::-moz-range-thumb]:border-0 focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-ac-accent)]" data-seek min="0" max="29" step="any" value="0" aria-label="Seek Copper Sky" aria-valuetext="0:00 of 0:29">
  <div class="mt-2 flex items-center justify-between">
    <span class="text-[.78rem] text-ac-muted tabular-nums" data-cur>0:00</span>
    <button type="button" class="grid size-[52px] flex-none cursor-pointer place-items-center rounded-full bg-ac-accent text-white shadow-[0_6px_16px_rgba(190,18,60,.32)] transition-transform duration-150 hover:scale-105 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ac-accent motion-reduce:transition-none motion-reduce:hover:scale-100" data-play="Copper Sky" aria-label="Play Copper Sky"><svg class="block size-6 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-6 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="text-[.78rem] text-ac-muted tabular-nums" data-dur>0:29</span>
  </div>
</div></div>

<div><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#6b6460] uppercase">Cover with overlay controls</span>
<div class="group relative aspect-square max-w-[320px] overflow-hidden rounded-[22px] text-white shadow-[0_1px_2px_rgba(28,25,23,.06),0_12px_32px_rgba(28,25,23,.1)] [--p:0] [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)]" data-ac role="group" aria-label="Night Bus to Nowhere by Orla Finch">
  <audio src="../audio/track-3.mp3" preload="metadata"></audio>
  <div class="absolute inset-x-0 bottom-0 bg-[linear-gradient(to_top,rgba(15,23,42,.92)_30%,rgba(15,23,42,0))] px-[18px] pt-[70px] pb-4">
    <div class="mb-4 flex items-center gap-4">
      <div class="min-w-0 flex-1"><p class="text-[1.05rem] font-bold tracking-[-.01em]">Night Bus to Nowhere</p><p class="mt-[3px] text-[.86rem] text-[#e2e8f0]">Orla Finch · Last Stop Songs</p></div>
      <button type="button" class="grid size-[52px] flex-none cursor-pointer place-items-center rounded-full bg-white text-[#1c1917] transition-transform duration-150 hover:scale-105 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white motion-reduce:transition-none motion-reduce:hover:scale-100" data-play="Night Bus to Nowhere" aria-label="Play Night Bus to Nowhere"><svg class="block size-6 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-6 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>
    <input type="range" class="w-full block h-[12px] 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,#fda4af_0_calc(6px_+_(100%_-_12px)_*_var(--f)),rgba(255,255,255,.5)_0)] [&::-moz-range-track]:h-[4px] [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,#fda4af_0_calc(6px_+_(100%_-_12px)_*_var(--f)),rgba(255,255,255,.5)_0)] [&::-webkit-slider-thumb]:-mt-[4px] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-[12px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[#fda4af] [&::-webkit-slider-thumb]:border-0 focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#0f172a,0_0_0_5px_#fda4af] [&::-moz-range-thumb]:size-[12px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-[#fda4af] [&::-moz-range-thumb]:border-0 focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#0f172a,0_0_0_5px_#fda4af]" data-seek min="0" max="30" step="any" value="0" aria-label="Seek Night Bus to Nowhere" aria-valuetext="0:00 of 0:30">
    <div class="mt-1 flex justify-between"><span class="text-[.78rem] text-[#e2e8f0] tabular-nums" data-cur>0:00</span><span class="text-[.78rem] text-[#e2e8f0] tabular-nums" data-dur>0:30</span></div>
  </div>
</div></div>

<div class="col-span-full"><span class="mb-3.5 block text-[.72rem] tracking-[.12em] text-[#6b6460] uppercase">Horizontal card</span>
<div class="group flex gap-[18px] rounded-[22px] bg-white p-3.5 text-[#1c1917] shadow-[0_1px_2px_rgba(28,25,23,.06),0_12px_32px_rgba(28,25,23,.1)] [--p:0] max-[480px]:flex-col" data-ac role="group" aria-label="Harbour Lights by Juniper Vale">
  <audio src="../audio/track-1.mp3" preload="metadata"></audio>
  <div class="aspect-square w-[132px] flex-none rounded-2xl [background:radial-gradient(circle_at_68%_40%,#fde68a_0_11%,transparent_11.5%),repeating-linear-gradient(180deg,transparent_0_6%,rgba(253,230,138,.3)_6%_7.5%)_bottom/100%_42%_no-repeat,linear-gradient(180deg,#fb923c,#e11d48_58%,#312e81_58%,#1e1b4b)] max-[480px]:aspect-[2/1] max-[480px]:w-full" aria-hidden="true"></div>
  <div class="flex min-w-0 flex-1 flex-col justify-center gap-4">
    <div><p class="text-[.7rem] font-bold tracking-[.12em] text-ac-accent uppercase">Now playing</p><p class="text-[1.05rem] font-bold tracking-[-.01em]">Harbour Lights</p><p class="mt-[3px] text-[.86rem] text-ac-muted">Juniper Vale · Low Tide Radio</p></div>
    <div class="flex items-center gap-4">
      <button type="button" class="grid size-11 flex-none cursor-pointer place-items-center rounded-full bg-ac-accent text-white shadow-[0_6px_16px_rgba(190,18,60,.32)] transition-transform duration-150 hover:scale-105 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ac-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="text-[.78rem] text-ac-muted tabular-nums" data-cur>0:00</span>
      <input type="range" class="w-full flex-1 block h-[12px] 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-ac-accent)_0_calc(6px_+_(100%_-_12px)_*_var(--f)),var(--color-ac-track)_0)] [&::-moz-range-track]:h-[4px] [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-ac-accent)_0_calc(6px_+_(100%_-_12px)_*_var(--f)),var(--color-ac-track)_0)] [&::-webkit-slider-thumb]:-mt-[4px] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-[12px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[color:var(--color-ac-accent)] [&::-webkit-slider-thumb]:border-0 focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-ac-accent)] [&::-moz-range-thumb]:size-[12px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-[color:var(--color-ac-accent)] [&::-moz-range-thumb]:border-0 focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-ac-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="text-[.78rem] text-ac-muted tabular-nums" data-dur>0:24</span>
      <button type="button" class="group/m grid size-[38px] flex-none cursor-pointer place-items-center rounded-full text-ac-muted hover:bg-[#f4f1ec] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ac-accent aria-pressed:text-ac-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>
    </div>
  </div>
</div></div>
CSS
@theme {
  --color-ac-accent: #be123c;
  --color-ac-muted: #57534e;
  --color-ac-track: #8a837d;
}
JavaScript
// Each [data-ac] card wraps one <audio>. The script wires its play button, seek slider, mute and like buttons.
(() => {
  const fmt = s => { s = Math.floor(s || 0); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
  document.querySelectorAll('[data-ac]').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();
    // Like: a plain toggle button, its state lives in aria-pressed.
    root.querySelectorAll('[data-like]').forEach(b => b.addEventListener('click', () => {
      b.setAttribute('aria-pressed', String(b.getAttribute('aria-pressed') !== 'true'));
    }));
  });
})();

About this set

Three card layouts that show the album art first. The vertical album card stacks a square cover, the title, the artist and album name, a heart-shaped like button, a seek slider and a large play button between the elapsed and total time. The cover with overlay controls fills the whole card with the artwork and lays a dark gradient across the bottom, which carries a white title, a white play button and the seek slider. The horizontal card puts the cover on the left and a Now playing label, title, artist, play button, seek slider, times and a mute button on the right; under 480 pixels it stacks and the cover turns into a wide banner. The cover art is drawn with CSS gradients only, a sunset, a kite, a moon and a banded sky, each in one background declaration, so the demo needs no image files; swap it for your own album art as a background image or an img element. Every card wraps a native audio element, and a short script syncs the play button, the seek slider and the time readouts, keeps the like button’s state in aria-pressed and pauses any other player when one starts.

What’s included

  • Vertical card, full-bleed cover with overlay controls and a horizontal card
  • Cover art drawn with CSS gradients, no image files
  • Like button with aria-pressed and a heart that fills
  • Overlay text kept readable by a dark gradient behind it
  • Horizontal card stacks under 480px
  • Bootstrap version built on .card, .btn and .form-range

Accessibility

Each card is a group named after the track and artist, and the cover art is hidden from screen readers because the title sits next to it. The play button's label switches between Play and Pause with the track name, the seek slider reports its position as text such as 0:12 of 0:29, and the like and mute buttons use aria-pressed. Keyboard focus shows a 2px outline or a two-colour ring on the slider thumb, and the hover scale is dropped under prefers-reduced-motion.

Customise it

Set --ac-accent, --ac-muted and --ac-track on .ac, and --art on any cover to change the artwork. In Bootstrap the same variables sit next to --bs-card-border-radius and the --bs-btn-* variables; in Tailwind edit the --color-ac-* theme colours and the background arbitrary value on each cover.