Podcast — Audio Player
A full dark-mode podcast player with a real HTML audio element, an animated gradient waveform you can scrub to seek, glowing play and pause controls, skip 15 seconds forward or back, adjustable playback speed from 1x to 2x, a volume slider with mute, live current-time and duration readouts, episode art and a chapter list.
MCP
Code
:root {
--bg: #0c0c10;
--surface: #15151c;
--surface-2: #1d1d27;
--violet: #8b5cf6;
--violet-d: #7c3aed;
--cyan: #22d3ee;
--pink: #f472b6;
--ink: #f4f4f8;
--muted: #9a9aab;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--white: #fff;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
min-height: 100vh;
background:
radial-gradient(900px 500px at 80% -10%, rgba(139, 92, 246, 0.22), transparent 60%),
radial-gradient(700px 480px at -10% 110%, rgba(34, 211, 238, 0.16), transparent 60%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.stage {
min-height: 100vh;
display: grid;
place-items: center;
padding: 32px 18px;
}
.player {
width: 100%;
max-width: 480px;
background: linear-gradient(180deg, var(--surface-2), var(--surface));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
}
/* ---- header ---- */
.player__head {
display: flex;
gap: 16px;
align-items: center;
}
.art {
position: relative;
width: 96px;
height: 96px;
flex: 0 0 auto;
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--violet-d), var(--violet) 45%, var(--cyan));
display: grid;
place-items: center;
overflow: hidden;
box-shadow: 0 12px 30px rgba(124, 58, 237, 0.4);
}
.art__glow {
position: absolute;
inset: -40% -40% auto auto;
width: 120%;
height: 120%;
background: radial-gradient(circle at 30% 20%, rgba(255, 255, 255, 0.5), transparent 55%);
opacity: 0.7;
}
.art__mono {
font-weight: 800;
font-size: 34px;
letter-spacing: 1px;
color: var(--white);
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
}
.art__badge {
position: absolute;
left: 8px;
bottom: 8px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.5px;
padding: 3px 7px;
border-radius: 999px;
background: rgba(12, 12, 16, 0.72);
color: var(--white);
backdrop-filter: blur(4px);
}
.meta { min-width: 0; }
.meta__show {
margin: 0;
font-size: 12px;
font-weight: 700;
letter-spacing: 1.4px;
text-transform: uppercase;
color: var(--cyan);
}
.meta__title {
margin: 4px 0 2px;
font-size: 20px;
font-weight: 700;
line-height: 1.25;
}
.meta__by {
margin: 0;
font-size: 13px;
color: var(--muted);
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px;
}
.tag {
font-size: 11px;
font-weight: 600;
padding: 3px 9px;
border-radius: 999px;
background: var(--surface);
border: 1px solid var(--line);
color: var(--muted);
}
.tag--live {
color: var(--bg);
background: linear-gradient(90deg, var(--violet), var(--cyan));
border-color: transparent;
}
/* ---- waveform ---- */
.wave {
position: relative;
margin-top: 22px;
height: 76px;
padding: 8px 4px;
border-radius: var(--r-md);
background: var(--surface);
border: 1px solid var(--line);
cursor: pointer;
outline: none;
overflow: hidden;
}
.wave:focus-visible { box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.55); }
.wave__bars {
display: flex;
align-items: center;
gap: 3px;
height: 100%;
width: 100%;
}
.bar {
flex: 1 1 auto;
min-width: 2px;
border-radius: 999px;
background: var(--line-2);
transition: background 0.12s ease, transform 0.12s ease;
}
.bar.is-played {
background: linear-gradient(180deg, var(--violet), var(--cyan));
box-shadow: 0 0 8px rgba(139, 92, 246, 0.55);
}
.bar.is-cursor { transform: scaleY(1.12); }
.wave__playhead {
position: absolute;
top: 6px;
bottom: 6px;
width: 2px;
left: 0;
background: var(--white);
border-radius: 2px;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
.wave.is-active .wave__playhead { opacity: 1; }
.times {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--muted);
}
.times__cur { color: var(--ink); font-weight: 600; }
/* ---- controls ---- */
.controls {
margin-top: 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 22px;
}
.btn {
position: relative;
display: inline-grid;
place-items: center;
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink);
cursor: pointer;
border-radius: 50%;
transition: transform 0.12s ease, background 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}
.btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.55); }
.btn:active { transform: scale(0.94); }
.btn--skip {
width: 52px;
height: 52px;
}
.btn--skip:hover { border-color: var(--line-2); background: var(--surface-2); }
.btn--skip svg { width: 26px; height: 26px; }
.btn__num {
position: absolute;
font-size: 9px;
font-weight: 700;
top: 50%;
left: 50%;
transform: translate(-50%, 1px);
pointer-events: none;
}
.btn--play {
width: 74px;
height: 74px;
background: linear-gradient(135deg, var(--violet-d), var(--violet) 55%, var(--cyan));
border-color: transparent;
color: var(--white);
box-shadow: 0 10px 30px rgba(124, 58, 237, 0.5), 0 0 0 6px rgba(139, 92, 246, 0.12);
}
.btn--play:hover { box-shadow: 0 12px 38px rgba(124, 58, 237, 0.7), 0 0 0 8px rgba(139, 92, 246, 0.18); }
.btn--play svg { width: 34px; height: 34px; }
.btn--play .ic-pause { display: none; }
.player.is-playing .btn--play .ic-play { display: none; }
.player.is-playing .btn--play .ic-pause { display: block; }
/* ---- speed + volume ---- */
.subrow {
margin-top: 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.chip {
font-family: inherit;
font-size: 13px;
font-weight: 700;
min-width: 58px;
padding: 8px 14px;
border-radius: 999px;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line);
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease, color 0.16s ease;
}
.chip:hover { border-color: var(--line-2); }
.chip:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.55); }
.chip.is-boost {
color: var(--bg);
background: linear-gradient(90deg, var(--violet), var(--cyan));
border-color: transparent;
}
.vol {
display: flex;
align-items: center;
gap: 10px;
flex: 1 1 auto;
max-width: 220px;
}
.vol__btn {
display: inline-grid;
place-items: center;
width: 38px;
height: 38px;
flex: 0 0 auto;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink);
cursor: pointer;
transition: border-color 0.16s ease, color 0.16s ease;
}
.vol__btn:hover { border-color: var(--line-2); }
.vol__btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.55); }
.vol__btn svg { width: 20px; height: 20px; }
.vol__btn .ic-mute { display: none; }
.vol.is-muted .vol__btn { color: var(--pink); }
.vol.is-muted .ic-vol { display: none; }
.vol.is-muted .ic-mute { display: block; }
.vol__track {
position: relative;
flex: 1 1 auto;
height: 6px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line);
cursor: pointer;
}
.vol__track:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.55); }
.vol__fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 80%;
border-radius: 999px;
background: linear-gradient(90deg, var(--violet), var(--cyan));
}
.vol__knob {
position: absolute;
top: 50%;
left: 80%;
width: 14px;
height: 14px;
transform: translate(-50%, -50%);
border-radius: 50%;
background: var(--white);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
/* ---- chapters ---- */
.chapters {
margin-top: 22px;
border-top: 1px solid var(--line);
padding-top: 14px;
}
.chapters__label {
margin: 0 0 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: 1.2px;
text-transform: uppercase;
color: var(--muted);
}
.chapters__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
max-height: 190px;
overflow-y: auto;
}
.chapter {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 9px 10px;
border: 1px solid transparent;
border-radius: var(--r-sm);
background: none;
color: var(--ink);
font-family: inherit;
font-size: 14px;
text-align: left;
cursor: pointer;
transition: background 0.14s ease, border-color 0.14s ease;
}
.chapter:hover { background: var(--surface); }
.chapter:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.45); }
.chapter__time {
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--muted);
min-width: 40px;
}
.chapter__name { flex: 1 1 auto; min-width: 0; }
.chapter__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--line-2);
flex: 0 0 auto;
}
.chapter.is-active {
background: linear-gradient(90deg, rgba(139, 92, 246, 0.18), rgba(34, 211, 238, 0.08));
border-color: rgba(139, 92, 246, 0.4);
}
.chapter.is-active .chapter__time { color: var(--cyan); font-weight: 600; }
.chapter.is-active .chapter__dot {
background: linear-gradient(90deg, var(--violet), var(--cyan));
box-shadow: 0 0 8px rgba(139, 92, 246, 0.7);
}
/* ---- toast ---- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
padding: 11px 18px;
border-radius: 999px;
background: rgba(29, 29, 39, 0.94);
border: 1px solid var(--line-2);
color: var(--ink);
font-size: 13px;
font-weight: 600;
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 20;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---- responsive ---- */
@media (max-width: 520px) {
.player { padding: 18px; }
.player__head {
flex-direction: column;
text-align: center;
align-items: center;
}
.tags { justify-content: center; }
.meta__title { font-size: 18px; }
.art { width: 120px; height: 120px; }
.controls { gap: 16px; }
.subrow { flex-direction: column; align-items: stretch; }
.vol { max-width: none; }
.chip { align-self: center; }
}(function () {
"use strict";
var audio = document.getElementById("audio");
var player = document.querySelector(".player");
var playBtn = document.getElementById("play");
var back15 = document.getElementById("back15");
var fwd15 = document.getElementById("fwd15");
var speedBtn = document.getElementById("speed");
var muteBtn = document.getElementById("mute");
var vol = document.querySelector(".vol");
var volTrack = document.getElementById("volTrack");
var volFill = document.getElementById("volFill");
var volKnob = document.getElementById("volKnob");
var wave = document.getElementById("wave");
var waveBars = document.getElementById("waveBars");
var playhead = document.getElementById("playhead");
var curEl = document.getElementById("current");
var durEl = document.getElementById("duration");
var chaptersEl = document.getElementById("chapters");
var toastEl = document.getElementById("toast");
var FALLBACK_DURATION = 120; // silence clip is ~2 min; used if metadata missing
var BAR_COUNT = 64;
var bars = [];
var chapters = [
{ t: 0, name: "Cold open — the myth of willpower" },
{ t: 18, name: "Designing a room that thinks for you" },
{ t: 42, name: "The 90-minute attention block" },
{ t: 66, name: "Notifications as a tax you keep paying" },
{ t: 90, name: "Listener questions & rituals" },
{ t: 108, name: "Closing thoughts and next week" }
];
var speeds = [1, 1.5, 2];
var speedIndex = 0;
var lastVolume = 0.8;
/* ---------- toast ---------- */
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 1600);
}
/* ---------- helpers ---------- */
function fmt(sec) {
if (!isFinite(sec) || sec < 0) sec = 0;
var m = Math.floor(sec / 60);
var s = Math.floor(sec % 60);
return m + ":" + (s < 10 ? "0" + s : s);
}
function getDuration() {
return isFinite(audio.duration) && audio.duration > 0
? audio.duration
: FALLBACK_DURATION;
}
/* ---------- build waveform ---------- */
function buildWave() {
var frag = document.createDocumentFragment();
for (var i = 0; i < BAR_COUNT; i++) {
var b = document.createElement("span");
b.className = "bar";
// pseudo-random but deterministic height profile
var h = 26 + Math.abs(Math.sin(i * 0.9) * 46) + Math.abs(Math.cos(i * 0.33) * 20);
b.style.height = Math.min(100, h) + "%";
frag.appendChild(b);
bars.push(b);
}
waveBars.appendChild(frag);
}
/* ---------- build chapters ---------- */
function buildChapters() {
chapters.forEach(function (ch, idx) {
var li = document.createElement("li");
var btn = document.createElement("button");
btn.type = "button";
btn.className = "chapter";
btn.dataset.index = idx;
btn.setAttribute("aria-label", "Jump to " + ch.name + " at " + fmt(ch.t));
btn.innerHTML =
'<span class="chapter__dot"></span>' +
'<span class="chapter__name"></span>' +
'<span class="chapter__time">' + fmt(ch.t) + "</span>";
btn.querySelector(".chapter__name").textContent = ch.name;
btn.addEventListener("click", function () {
audio.currentTime = Math.min(ch.t, getDuration() - 0.2);
if (audio.paused) audio.play();
toast("Chapter: " + ch.name);
});
li.appendChild(btn);
chaptersEl.appendChild(li);
});
}
function activeChapter(time) {
var active = 0;
for (var i = 0; i < chapters.length; i++) {
if (time >= chapters[i].t) active = i;
}
var nodes = chaptersEl.querySelectorAll(".chapter");
nodes.forEach(function (n) {
n.classList.toggle("is-active", Number(n.dataset.index) === active);
});
}
/* ---------- progress render ---------- */
function render() {
var dur = getDuration();
var pct = dur ? (audio.currentTime / dur) : 0;
pct = Math.max(0, Math.min(1, pct));
var filled = Math.round(pct * BAR_COUNT);
for (var i = 0; i < BAR_COUNT; i++) {
bars[i].classList.toggle("is-played", i < filled);
}
playhead.style.left = (pct * 100) + "%";
curEl.textContent = fmt(audio.currentTime);
durEl.textContent = fmt(dur);
wave.setAttribute("aria-valuenow", Math.round(pct * 100));
wave.setAttribute("aria-valuetext", Math.round(pct * 100) + " percent");
activeChapter(audio.currentTime);
}
/* ---------- play / pause ---------- */
function togglePlay() {
if (audio.paused) {
audio.play();
} else {
audio.pause();
}
}
audio.addEventListener("play", function () {
player.classList.add("is-playing");
wave.classList.add("is-active");
playBtn.setAttribute("aria-pressed", "true");
playBtn.setAttribute("aria-label", "Pause episode");
});
audio.addEventListener("pause", function () {
player.classList.remove("is-playing");
playBtn.setAttribute("aria-pressed", "false");
playBtn.setAttribute("aria-label", "Play episode");
});
audio.addEventListener("ended", function () {
wave.classList.remove("is-active");
toast("Episode finished");
});
audio.addEventListener("timeupdate", render);
audio.addEventListener("loadedmetadata", render);
audio.addEventListener("durationchange", render);
playBtn.addEventListener("click", togglePlay);
back15.addEventListener("click", function () {
audio.currentTime = Math.max(0, audio.currentTime - 15);
toast("−15 seconds");
});
fwd15.addEventListener("click", function () {
audio.currentTime = Math.min(getDuration() - 0.2, audio.currentTime + 15);
toast("+15 seconds");
});
/* ---------- speed ---------- */
speedBtn.addEventListener("click", function () {
speedIndex = (speedIndex + 1) % speeds.length;
var sp = speeds[speedIndex];
audio.playbackRate = sp;
speedBtn.textContent = sp + "×";
speedBtn.classList.toggle("is-boost", sp !== 1);
toast("Speed " + sp + "×");
});
/* ---------- volume ---------- */
function setVolume(v) {
v = Math.max(0, Math.min(1, v));
audio.volume = v;
audio.muted = v === 0;
volFill.style.width = (v * 100) + "%";
volKnob.style.left = (v * 100) + "%";
volTrack.setAttribute("aria-valuenow", Math.round(v * 100));
vol.classList.toggle("is-muted", v === 0);
if (v > 0) lastVolume = v;
}
function volFromEvent(clientX) {
var r = volTrack.getBoundingClientRect();
setVolume((clientX - r.left) / r.width);
}
var draggingVol = false;
volTrack.addEventListener("pointerdown", function (e) {
draggingVol = true;
volTrack.setPointerCapture(e.pointerId);
volFromEvent(e.clientX);
});
volTrack.addEventListener("pointermove", function (e) {
if (draggingVol) volFromEvent(e.clientX);
});
volTrack.addEventListener("pointerup", function () { draggingVol = false; });
volTrack.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight" || e.key === "ArrowUp") {
setVolume(audio.volume + 0.05); e.preventDefault();
} else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
setVolume(audio.volume - 0.05); e.preventDefault();
}
});
muteBtn.addEventListener("click", function () {
if (audio.volume === 0 || audio.muted) {
setVolume(lastVolume || 0.8);
toast("Unmuted");
} else {
setVolume(0);
toast("Muted");
}
});
/* ---------- waveform seek ---------- */
function seekFromEvent(clientX) {
var r = wave.getBoundingClientRect();
var pct = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
audio.currentTime = pct * getDuration();
render();
}
function markCursor(clientX) {
var r = waveBars.getBoundingClientRect();
var pct = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
var idx = Math.round(pct * (BAR_COUNT - 1));
for (var i = 0; i < BAR_COUNT; i++) {
bars[i].classList.toggle("is-cursor", i === idx);
}
}
var draggingWave = false;
wave.addEventListener("pointerdown", function (e) {
draggingWave = true;
wave.setPointerCapture(e.pointerId);
seekFromEvent(e.clientX);
});
wave.addEventListener("pointermove", function (e) {
markCursor(e.clientX);
if (draggingWave) seekFromEvent(e.clientX);
});
wave.addEventListener("pointerup", function () { draggingWave = false; });
wave.addEventListener("pointerleave", function () {
bars.forEach(function (b) { b.classList.remove("is-cursor"); });
});
wave.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight") {
audio.currentTime = Math.min(getDuration() - 0.2, audio.currentTime + 5);
e.preventDefault();
} else if (e.key === "ArrowLeft") {
audio.currentTime = Math.max(0, audio.currentTime - 5);
e.preventDefault();
} else if (e.key === " " || e.key === "Enter") {
togglePlay();
e.preventDefault();
}
});
/* ---------- global keyboard ---------- */
document.addEventListener("keydown", function (e) {
var tag = (e.target && e.target.tagName) || "";
var isControl = /BUTTON|INPUT/.test(tag) ||
(e.target && e.target.getAttribute("role") === "slider");
if (e.key === " " && !isControl) {
togglePlay();
e.preventDefault();
}
});
/* ---------- init ---------- */
buildWave();
buildChapters();
setVolume(0.8);
audio.playbackRate = 1;
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Podcast — Audio Player</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="stage">
<section class="player" aria-label="Podcast audio player">
<header class="player__head">
<div class="art" aria-hidden="true">
<div class="art__glow"></div>
<span class="art__mono">SR</span>
<span class="art__badge">EP 142</span>
</div>
<div class="meta">
<p class="meta__show">Signal & Reverb</p>
<h1 class="meta__title">The Quiet Architecture of Focus</h1>
<p class="meta__by">with Mara Delacroix & Theo Nkemelu</p>
<div class="tags">
<span class="tag">Deep Work</span>
<span class="tag">48 min</span>
<span class="tag tag--live">New</span>
</div>
</div>
</header>
<div class="wave" id="wave" role="slider" tabindex="0"
aria-label="Seek through episode"
aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
aria-valuetext="0 percent">
<div class="wave__bars" id="waveBars"></div>
<div class="wave__playhead" id="playhead"></div>
</div>
<div class="times">
<span id="current" class="times__cur">0:00</span>
<span id="duration" class="times__dur">0:00</span>
</div>
<div class="controls">
<button class="btn btn--skip" id="back15" type="button" aria-label="Skip back 15 seconds">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5V2L7 6l5 4V7a5 5 0 1 1-5 5H5a7 7 0 1 0 7-7z" fill="currentColor"/></svg>
<span class="btn__num">15</span>
</button>
<button class="btn btn--play" id="play" type="button" aria-label="Play episode" aria-pressed="false">
<svg class="ic-play" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
<svg class="ic-pause" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5h4v14H7zM13 5h4v14h-4z" fill="currentColor"/></svg>
</button>
<button class="btn btn--skip" id="fwd15" type="button" aria-label="Skip forward 15 seconds">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5V2l5 4-5 4V7a5 5 0 1 0 5 5h2a7 7 0 1 1-7-7z" fill="currentColor"/></svg>
<span class="btn__num">15</span>
</button>
</div>
<div class="subrow">
<button class="chip" id="speed" type="button" aria-label="Playback speed">1×</button>
<div class="vol">
<button class="vol__btn" id="mute" type="button" aria-label="Mute">
<svg class="ic-vol" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9v6h4l5 5V4L8 9H4z" fill="currentColor"/><path d="M16 8a5 5 0 0 1 0 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<svg class="ic-mute" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9v6h4l5 5V4L8 9H4z" fill="currentColor"/><path d="M16 9l5 6M21 9l-5 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
<div class="vol__track" id="volTrack" role="slider" tabindex="0"
aria-label="Volume" aria-valuemin="0" aria-valuemax="100" aria-valuenow="80">
<div class="vol__fill" id="volFill"></div>
<div class="vol__knob" id="volKnob"></div>
</div>
</div>
</div>
<div class="chapters">
<p class="chapters__label">Chapters</p>
<ul class="chapters__list" id="chapters"></ul>
</div>
<audio id="audio" preload="metadata"
src="https://cdn.jsdelivr.net/gh/anars/blank-audio@master/2-minutes-of-silence.mp3"></audio>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A production-ready podcast audio player built around a real <audio> element, so every control drives genuine playback. The centerpiece is an animated waveform rendered as a row of gradient bars (violet to cyan); the played portion glows while the unplayed portion dims, and clicking or dragging anywhere on the waveform seeks the track. A large glowing play/pause button sits beside skip-back and skip-forward controls that jump 15 seconds each way.
Below the transport, a speed toggle cycles through 1x, 1.5x and 2x, and a volume slider with a mute button controls output level with a live fill indicator. Current time and total duration update every frame, and the episode art, title and show name anchor the card. A scrollable chapter list lets listeners jump straight to a timestamped segment, with the active chapter highlighted as playback moves.
Everything is keyboard-usable with visible focus rings, announced through ARIA labels, and responsive down to 360px where the layout stacks the art above the controls. A small toast confirms actions like changing speed or muting. No frameworks, no build step — just HTML, CSS and vanilla JavaScript.