Open demo

CSS code

HTML
<article class="player">
  <div class="art"><b>Now playing</b></div>
  <h2>Weightless in Lisbon</h2>
  <p class="artist">Marta Reis · Coastal Frequencies</p>
  <label class="sr-only" for="seek">Seek</label>
  <input id="seek" type="range" min="0" max="228" value="94" aria-valuetext="1:34 of 3:48" data-seek>
  <div class="times" aria-hidden="true"><span data-elapsed>1:34</span><span>3:48</span></div>
  <div class="ctrl">
    <button type="button" aria-label="Previous track"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5h2v14H7zM19 5v14l-9-7z"/></svg></button>
    <button class="play" type="button" aria-label="Pause"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5h3v14H8zM13 5h3v14h-3z"/></svg></button>
    <button type="button" aria-label="Next track"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 5h2v14h-2zM5 5l9 7-9 7z"/></svg></button>
  </div>
</article>
CSS
/* Now-playing card. The scrubber is a real <input type="range"> coloured with accent-color, so it
   keeps native keyboard support (arrows, Home, End); a few lines of script keep the elapsed time
   and aria-valuetext ("1:34 of 3:48") in step with it.
   The artwork is a CSS gradient; swap it for an <img alt=""> when you have real cover art. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.player{--accent:#f43f5e;width:330px;max-width:100%;border-radius:20px;padding:20px;background:#161a23;
  border:1px solid #262c39;color:#e8eaf1}
.player svg{display:block}
.player .art{height:190px;border-radius:14px;margin-bottom:18px;position:relative;overflow:hidden;
  background:linear-gradient(140deg,#f43f5e,#a855f7 48%,#3b82f6)}
.player .art::after{content:"";position:absolute;inset:0;
  background:radial-gradient(52% 48% at 26% 24%,rgba(255,255,255,.4),transparent 62%),
             radial-gradient(46% 44% at 78% 78%,rgba(0,0,0,.34),transparent 60%)}
.player .art b{position:absolute;left:16px;bottom:14px;z-index:1;font-size:.68rem;font-weight:800;
  letter-spacing:.14em;text-transform:uppercase;color:#fff;background:rgba(14,17,23,.3);
  padding:4px 8px;border-radius:6px}
.player h2{font-size:1.05rem;letter-spacing:-.01em}
.player .artist{font-size:.86rem;color:#98a0b3;margin-top:3px}
.player input[type=range]{display:block;width:100%;margin:18px 0 6px;accent-color:var(--accent);height:4px;cursor:pointer;border-radius:4px}
.player input[type=range]:focus-visible{outline:2px solid var(--accent);outline-offset:6px}
.player .times{display:flex;justify-content:space-between;font-size:.74rem;color:#98a0b3;
  font-variant-numeric:tabular-nums}
.player .ctrl{display:flex;align-items:center;justify-content:center;gap:20px;margin-top:16px}
.player .ctrl button{border:0;background:none;cursor:pointer;color:#c7cddd;display:grid;place-items:center;
  width:36px;height:36px;border-radius:50%;transition:color .15s,background-color .15s}
.player .ctrl button:hover{color:#fff}
.player .ctrl button:focus-visible{outline:2px solid #fff;outline-offset:2px}
.player .ctrl svg{width:22px;height:22px;fill:currentColor}
.player .ctrl .play{width:52px;height:52px;background:var(--accent);color:#fff}
.player .ctrl .play:hover{background:#e11d48}
.player .ctrl .play svg{width:20px;height:20px}
@media (prefers-reduced-motion:reduce){.player .ctrl button{transition:none}}
JavaScript
// Keeps the elapsed time and the announced value in step with the scrubber.
document.querySelectorAll('[data-seek]').forEach(input => {
  const elapsed = input.closest('.player').querySelector('[data-elapsed]');
  const fmt = s => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
  const sync = () => {
    const now = fmt(Number(input.value));
    elapsed.textContent = now;
    input.setAttribute('aria-valuetext', `${now} of ${fmt(Number(input.max))}`);
  };
  input.addEventListener('input', sync);
  sync();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<article class="card player">
  <div class="card-body">
    <div class="art"><span class="badge text-uppercase">Now playing</span></div>
    <h2 class="card-title">Weightless in Lisbon</h2>
    <p class="artist mb-0">Marta Reis · Coastal Frequencies</p>
    <label class="visually-hidden" for="seek">Seek</label>
    <input id="seek" class="form-range" type="range" min="0" max="228" value="94" aria-valuetext="1:34 of 3:48" data-seek>
    <div class="times d-flex justify-content-between" aria-hidden="true"><span data-elapsed>1:34</span><span>3:48</span></div>
    <div class="ctrl d-flex align-items-center justify-content-center">
      <button class="btn" type="button" aria-label="Previous track"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5h2v14H7zM19 5v14l-9-7z"/></svg></button>
      <button class="btn play" type="button" aria-label="Pause"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5h3v14H8zM13 5h3v14h-3z"/></svg></button>
      <button class="btn" type="button" aria-label="Next track"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 5h2v14h-2zM5 5l9 7-9 7z"/></svg></button>
    </div>
  </div>
</article>
CSS
/* Bootstrap .card + .badge + .form-range + .btn. The range keeps native keyboard support; its track
   is filled up to --pct, which the script updates together with the elapsed time and aria-valuetext. */
.player{--accent:#f43f5e;--bs-card-bg:#161a23;--bs-card-border-color:#262c39;--bs-card-border-radius:20px;
  --bs-card-color:#e8eaf1;--bs-card-spacer-y:20px;--bs-card-spacer-x:20px;width:330px;max-width:100%}
.player svg{display:block}
.player .art{height:190px;border-radius:14px;margin-bottom:18px;position:relative;overflow:hidden;
  background:linear-gradient(140deg,#f43f5e,#a855f7 48%,#3b82f6)}
.player .art::after{content:"";position:absolute;inset:0;
  background:radial-gradient(52% 48% at 26% 24%,rgba(255,255,255,.4),transparent 62%),
             radial-gradient(46% 44% at 78% 78%,rgba(0,0,0,.34),transparent 60%)}
.player .art .badge{--bs-badge-font-size:.68rem;--bs-badge-font-weight:800;--bs-badge-padding-x:8px;--bs-badge-padding-y:4px;
  --bs-badge-border-radius:6px;position:absolute;left:16px;bottom:14px;z-index:1;letter-spacing:.14em;
  background:rgba(14,17,23,.3);line-height:normal}
.player .card-title{font-size:1.05rem;font-weight:700;line-height:normal;letter-spacing:-.01em;margin:0}
.player .artist{font-size:.86rem;color:#98a0b3;margin-top:3px}
.player .form-range{--pct:41%;height:16px;margin:12px 0 0;display:block}
.player .form-range::-webkit-slider-runnable-track{height:4px;border-radius:4px;
  background:linear-gradient(to right,var(--accent) var(--pct),#e4e6eb 0)}
.player .form-range::-webkit-slider-thumb{width:16px;height:16px;margin-top:-6px;background:var(--accent)}
.player .form-range::-webkit-slider-thumb:active{background:#fb7185}
.player .form-range::-moz-range-track{height:4px;border-radius:4px;background:#e4e6eb}
.player .form-range::-moz-range-progress{height:4px;border-radius:4px;background:var(--accent)}
.player .form-range::-moz-range-thumb{width:16px;height:16px;background:var(--accent)}
.player .form-range::-moz-range-thumb:active{background:#fb7185}
.player .form-range:focus::-webkit-slider-thumb{box-shadow:none}
.player .form-range:focus::-moz-range-thumb{box-shadow:none}
.player .form-range:focus-visible{outline:2px solid var(--accent);outline-offset:4px;border-radius:4px}
.player .times{font-size:.74rem;color:#98a0b3;font-variant-numeric:tabular-nums}
.player .ctrl{gap:20px;margin-top:16px}
.player .ctrl .btn{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-border-width:0;--bs-btn-border-radius:50%;
  --bs-btn-color:#c7cddd;--bs-btn-hover-color:#fff;--bs-btn-active-color:#fff;--bs-btn-focus-box-shadow:0 0 0 2px #161a23,0 0 0 4px #fff;
  width:36px;height:36px;display:grid;place-items:center}
.player .ctrl svg{width:22px;height:22px;fill:currentColor}
.player .ctrl .play{--bs-btn-bg:var(--accent);--bs-btn-color:#fff;--bs-btn-hover-bg:#e11d48;--bs-btn-active-bg:#e11d48;width:52px;height:52px}
.player .ctrl .play svg{width:20px;height:20px}
@media (prefers-reduced-motion:reduce){.player .btn,.player .form-range::-webkit-slider-thumb{transition:none}}
JavaScript
// Keeps the elapsed time, the track fill (--pct) and the announced value in step with the scrubber.
document.querySelectorAll('[data-seek]').forEach(input => {
  const elapsed = input.closest('.player').querySelector('[data-elapsed]');
  const fmt = s => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
  const sync = () => {
    const now = fmt(Number(input.value));
    elapsed.textContent = now;
    input.setAttribute('aria-valuetext', `${now} of ${fmt(Number(input.max))}`);
    input.style.setProperty('--pct', `${(input.value / input.max) * 100}%`);
  };
  input.addEventListener('input', sync);
  sync();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<article class="player w-[330px] max-w-full rounded-[20px] border border-[#262c39] bg-[#161a23] p-5 text-[#e8eaf1]">
  <div class="relative mb-[18px] h-[190px] overflow-hidden rounded-[14px] bg-[linear-gradient(140deg,#f43f5e,#a855f7_48%,#3b82f6)] after:absolute after:inset-0 after:bg-[radial-gradient(52%_48%_at_26%_24%,rgba(255,255,255,.4),transparent_62%),radial-gradient(46%_44%_at_78%_78%,rgba(0,0,0,.34),transparent_60%)]"><b class="absolute bottom-3.5 left-4 z-10 rounded-md bg-[#0e1117]/30 px-2 py-1 text-[.68rem] font-extrabold tracking-[.14em] text-white uppercase">Now playing</b></div>
  <h2 class="text-[1.05rem] font-bold tracking-[-.01em]">Weightless in Lisbon</h2>
  <p class="mt-[3px] text-[.86rem] text-[#98a0b3]">Marta Reis · Coastal Frequencies</p>
  <label class="sr-only" for="seek">Seek</label>
  <input id="seek" class="mt-[18px] mb-1.5 block h-1 w-full cursor-pointer rounded accent-[#f43f5e] focus-visible:outline-2 focus-visible:outline-offset-[6px] focus-visible:outline-[#f43f5e]" type="range" min="0" max="228" value="94" aria-valuetext="1:34 of 3:48" data-seek>
  <div class="flex justify-between text-[.74rem] text-[#98a0b3] tabular-nums" aria-hidden="true"><span data-elapsed>1:34</span><span>3:48</span></div>
  <div class="mt-4 flex items-center justify-center gap-5">
    <button class="grid size-9 cursor-pointer place-items-center rounded-full text-[#c7cddd] transition-colors duration-150 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white motion-reduce:transition-none" type="button" aria-label="Previous track"><svg class="block size-[22px] fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5h2v14H7zM19 5v14l-9-7z"/></svg></button>
    <button class="grid size-[52px] cursor-pointer place-items-center rounded-full bg-[#f43f5e] text-white transition-colors duration-150 hover:bg-[#e11d48] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white motion-reduce:transition-none" type="button" aria-label="Pause"><svg class="block size-5 fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5h3v14H8zM13 5h3v14h-3z"/></svg></button>
    <button class="grid size-9 cursor-pointer place-items-center rounded-full text-[#c7cddd] transition-colors duration-150 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white motion-reduce:transition-none" type="button" aria-label="Next track"><svg class="block size-[22px] fill-current" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 5h2v14h-2zM5 5l9 7-9 7z"/></svg></button>
  </div>
</article>
JavaScript
// Keeps the elapsed time and the announced value in step with the scrubber.
document.querySelectorAll('[data-seek]').forEach(input => {
  const elapsed = input.closest('.player').querySelector('[data-elapsed]');
  const fmt = s => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`;
  const sync = () => {
    const now = fmt(Number(input.value));
    elapsed.textContent = now;
    input.setAttribute('aria-valuetext', `${now} of ${fmt(Number(input.max))}`);
  };
  input.addEventListener('input', sync);
  sync();
});

About this set

A dark now-playing card for a music or podcast player: gradient cover art with a Now playing tag, the track title, the artist and album, a seek bar with elapsed and total time, and previous, pause and next controls with a large pink pause button in the middle. The seek bar is a real input type=”range”, coloured with accent-color in the plain and Tailwind versions, so it keeps native keyboard control with the arrow keys, Home and End, and native touch dragging. A few lines of script update the elapsed time and the aria-valuetext as it moves, so a screen reader announces 1:34 of 3:48 rather than a raw number of seconds. The control buttons are 36px targets with labels. The card does not play audio by itself; wire the buttons and the range to an audio element. The Bootstrap version uses the stock .form-range, restyled so its track fills up to the thumb through a –pct variable the same script sets, plus .badge and .btn; the Tailwind version uses accent- utilities on the native range.

What’s included

  • Seek bar is a native range input with keyboard support
  • Elapsed time and aria-valuetext kept in sync by script
  • Labelled previous, pause and next buttons
  • Bootstrap .form-range with a filled track
  • Cover art drawn with CSS gradients

Accessibility

The range has a label, Seek, and an aria-valuetext such as 1:34 of 3:48 that updates as it moves; the visible times are aria-hidden to avoid repeating it. The icon buttons are labelled Previous track, Pause and Next track, show a white focus outline, and the range shows a pink outline on keyboard focus. The button colour transition is removed under reduced motion.

Customise it

--accent sets the range and play button colour (a class value in Tailwind). Set max to the track length in seconds and value to the position; the script formats both. Replace the .art gradient with an img alt="" for real cover art.