Music — Sticky Bottom Player Bar
A polished, sticky bottom now-playing player bar with a CSS-drawn album cover, animated equalizer, and a morphing play/pause button. Transport controls cover shuffle, previous, next, and repeat, while a click-and-drag scrubber advances current and total time during simulated playback. The right side adds a volume slider with mute, a queue toggle, and full-screen expand — all keyboard-accessible with ARIA slider and pressed states.
MCP
Kod
:root {
--bg: #0b0b0f;
--bg-2: #13131a;
--surface: #1a1a22;
--surface-2: #22222c;
--text: #f4f4f7;
--muted: #a0a0ad;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.18);
--accent: #1db954;
--accent-2: #8b5cf6;
--accent-3: #ff3d71;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-full: 999px;
--shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
--bar-h: 92px;
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1200px 600px at 80% -10%, rgba(139, 92, 246, 0.16), transparent 60%),
radial-gradient(1000px 500px at 0% 0%, rgba(29, 185, 84, 0.12), transparent 55%),
var(--bg);
color: var(--text);
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding-bottom: calc(var(--bar-h) + 28px);
}
/* ---------- page stage ---------- */
.stage {
max-width: 880px;
margin: 0 auto;
padding: 56px 22px 24px;
}
.eyebrow {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.stage__head h1 {
font-family: Sora, Inter, sans-serif;
font-weight: 800;
font-size: clamp(34px, 7vw, 64px);
line-height: 1.02;
letter-spacing: -0.02em;
margin: 10px 0 12px;
background: linear-gradient(120deg, var(--text), #c9c4ff 70%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.byline {
color: var(--muted);
margin: 0;
font-size: 15px;
}
.byline strong {
color: var(--text);
}
/* ---------- queue ---------- */
.queue {
margin-top: 40px;
}
.queue__title {
font-family: Sora, Inter, sans-serif;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
margin: 0 0 12px;
}
.queue__list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 6px;
}
.q-row {
display: grid;
grid-template-columns: 26px 44px 1fr auto auto;
align-items: center;
gap: 14px;
padding: 8px 12px;
border: 1px solid transparent;
border-radius: var(--r-md);
cursor: pointer;
transition: background 0.18s ease, border-color 0.18s ease, transform 0.1s ease;
}
.q-row:hover {
background: var(--surface);
border-color: var(--line);
}
.q-row:active {
transform: scale(0.995);
}
.q-row.is-current {
background: linear-gradient(90deg, color-mix(in srgb, var(--ca) 22%, var(--surface)), var(--surface));
border-color: color-mix(in srgb, var(--ca) 40%, transparent);
}
.q-row__num {
color: var(--muted);
font-variant-numeric: tabular-nums;
font-size: 14px;
text-align: center;
}
.q-row.is-current .q-row__num {
color: var(--ca);
}
.q-row__cover {
width: 44px;
height: 44px;
border-radius: 10px;
background: linear-gradient(135deg, var(--ca), color-mix(in srgb, var(--ca) 30%, #000));
position: relative;
overflow: hidden;
box-shadow: inset 0 0 0 1px var(--line);
}
.q-row__cover::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(60% 60% at 30% 25%, rgba(255, 255, 255, 0.45), transparent 60%);
mix-blend-mode: screen;
}
.q-row__meta {
min-width: 0;
}
.q-row__t {
font-weight: 600;
font-size: 14.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.q-row.is-current .q-row__t {
color: var(--ca);
}
.q-row__a {
color: var(--muted);
font-size: 12.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.q-row__plays {
color: var(--muted);
font-size: 12.5px;
font-variant-numeric: tabular-nums;
}
.q-row__dur {
color: var(--muted);
font-size: 13px;
font-variant-numeric: tabular-nums;
}
/* ---------- sticky player ---------- */
.player {
position: fixed;
inset: auto 0 0 0;
z-index: 40;
padding: 12px;
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.25) 30%);
}
.player__inner {
--ca: var(--cover-a);
max-width: 1280px;
margin: 0 auto;
display: grid;
grid-template-columns: minmax(190px, 1fr) minmax(280px, 2fr) minmax(190px, 1fr);
align-items: center;
gap: 16px;
height: var(--bar-h);
padding: 0 18px;
border-radius: var(--r-lg);
border: 1px solid var(--line);
background:
linear-gradient(180deg, color-mix(in srgb, var(--cover-a) 9%, var(--surface)), var(--surface));
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
box-shadow: var(--shadow);
}
/* LEFT */
.now {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.now__cover {
position: relative;
width: 56px;
height: 56px;
flex: none;
border-radius: var(--r-md);
overflow: hidden;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4), inset 0 0 0 1px var(--line);
}
.cover-art {
position: absolute;
inset: 0;
background: linear-gradient(135deg, var(--cover-a), var(--cover-b));
}
.cover-art__blob {
position: absolute;
width: 46px;
height: 46px;
border-radius: var(--r-full);
background: rgba(255, 255, 255, 0.55);
filter: blur(4px);
top: -10px;
left: -8px;
mix-blend-mode: soft-light;
}
.cover-art__blob--2 {
background: var(--cover-b);
top: auto;
left: auto;
bottom: -14px;
right: -10px;
width: 40px;
height: 40px;
filter: blur(6px);
mix-blend-mode: screen;
}
.cover-art__ring {
position: absolute;
inset: 14px;
border-radius: var(--r-full);
border: 2px solid rgba(255, 255, 255, 0.35);
}
.eq {
position: absolute;
inset: 0;
display: none;
align-items: flex-end;
justify-content: center;
gap: 3px;
padding: 10px;
background: rgba(0, 0, 0, 0.35);
}
.player.is-playing .eq {
display: flex;
}
.eq i {
width: 4px;
height: 8px;
border-radius: 2px;
background: #fff;
animation: eq 0.9s ease-in-out infinite;
}
.eq i:nth-child(1) { animation-delay: 0s; }
.eq i:nth-child(2) { animation-delay: 0.2s; }
.eq i:nth-child(3) { animation-delay: 0.4s; }
.eq i:nth-child(4) { animation-delay: 0.1s; }
@keyframes eq {
0%, 100% { height: 7px; }
50% { height: 24px; }
}
.now__meta {
min-width: 0;
}
.now__title {
font-weight: 700;
font-size: 14.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.now__artist {
color: var(--muted);
font-size: 12.5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* CENTER */
.transport {
display: grid;
gap: 8px;
justify-items: center;
}
.transport__btns {
display: flex;
align-items: center;
gap: 8px;
}
.scrub {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
max-width: 560px;
}
.scrub__time {
color: var(--muted);
font-size: 12px;
font-variant-numeric: tabular-nums;
min-width: 34px;
text-align: center;
}
.scrub__bar {
position: relative;
flex: 1;
height: 6px;
border-radius: var(--r-full);
background: var(--surface-2);
cursor: pointer;
touch-action: none;
}
.scrub__bar:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 4px;
}
.scrub__fill {
position: absolute;
inset: 0 auto 0 0;
width: 0%;
border-radius: var(--r-full);
background: linear-gradient(90deg, var(--ca), color-mix(in srgb, var(--ca) 55%, #fff));
}
.scrub__thumb {
position: absolute;
top: 50%;
left: 0%;
width: 13px;
height: 13px;
border-radius: var(--r-full);
background: #fff;
transform: translate(-50%, -50%) scale(0);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
transition: transform 0.14s ease;
}
.scrub__bar:hover .scrub__thumb,
.scrub__bar:focus-visible .scrub__thumb,
.scrub__bar.is-drag .scrub__thumb {
transform: translate(-50%, -50%) scale(1);
}
/* RIGHT */
.extras {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.vol {
position: relative;
width: 92px;
height: 6px;
border-radius: var(--r-full);
background: var(--surface-2);
cursor: pointer;
touch-action: none;
}
.vol:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 4px;
}
.vol__fill {
position: absolute;
inset: 0 auto 0 0;
width: 80%;
border-radius: var(--r-full);
background: var(--text);
}
.vol__thumb {
position: absolute;
top: 50%;
left: 80%;
width: 12px;
height: 12px;
border-radius: var(--r-full);
background: #fff;
transform: translate(-50%, -50%) scale(0);
transition: transform 0.14s ease;
}
.vol:hover .vol__thumb,
.vol:focus-visible .vol__thumb,
.vol.is-drag .vol__thumb {
transform: translate(-50%, -50%) scale(1);
}
.vol:hover .vol__fill,
.vol:focus-visible .vol__fill {
background: var(--ca);
}
/* ---------- buttons ---------- */
.icon-btn {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: var(--r-full);
border: none;
background: transparent;
color: var(--muted);
cursor: pointer;
transition: color 0.15s ease, background 0.15s ease, transform 0.1s ease;
}
.icon-btn:hover {
color: var(--text);
background: var(--surface-2);
}
.icon-btn:active {
transform: scale(0.92);
}
.icon-btn:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.icon-btn svg {
width: 19px;
height: 19px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.t-toggle[aria-pressed="true"] {
color: var(--ca);
}
.t-toggle[aria-pressed="true"]::after {
content: "";
position: absolute;
bottom: 4px;
width: 4px;
height: 4px;
border-radius: var(--r-full);
background: var(--ca);
}
.t-toggle {
position: relative;
}
.like .i-heart {
fill: none;
stroke: currentColor;
stroke-width: 1.8;
width: 19px;
height: 19px;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.like[aria-pressed="true"] {
color: var(--accent-3);
}
.like[aria-pressed="true"] .i-heart {
fill: var(--accent-3);
transform: scale(1.12);
}
.play-btn {
position: relative;
display: grid;
place-items: center;
width: 50px;
height: 50px;
border-radius: var(--r-full);
border: none;
cursor: pointer;
background: var(--text);
color: #0b0b0f;
box-shadow: 0 8px 22px rgba(255, 255, 255, 0.12);
transition: transform 0.12s ease, box-shadow 0.2s ease;
}
.play-btn:hover {
transform: scale(1.06);
}
.play-btn:active {
transform: scale(0.96);
}
.play-btn:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 3px;
}
.play-btn svg {
position: absolute;
width: 22px;
height: 22px;
fill: currentColor;
transition: opacity 0.18s ease, transform 0.18s ease;
}
.play-btn .i-pause {
opacity: 0;
transform: scale(0.6);
}
.player.is-playing .play-btn .i-play {
opacity: 0;
transform: scale(0.6);
}
.player.is-playing .play-btn .i-pause {
opacity: 1;
transform: scale(1);
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: calc(var(--bar-h) + 24px);
transform: translate(-50%, 12px);
z-index: 60;
padding: 10px 16px;
border-radius: var(--r-full);
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--text);
font-size: 13.5px;
font-weight: 600;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- responsive ---------- */
@media (max-width: 880px) {
.player__inner {
grid-template-columns: 1fr auto;
grid-template-areas:
"now extras"
"transport transport";
height: auto;
padding: 12px 14px;
gap: 10px;
}
.now { grid-area: now; }
.extras { grid-area: extras; }
.transport { grid-area: transport; }
:root { --bar-h: 138px; }
}
@media (max-width: 520px) {
.stage {
padding: 36px 16px 18px;
}
.extras .vol,
.extras #muteBtn,
.extras #expandBtn {
display: none;
}
.q-row {
grid-template-columns: 22px 40px 1fr auto;
gap: 10px;
}
.q-row__plays {
display: none;
}
.scrub {
max-width: none;
}
.player__inner {
padding: 10px 12px;
}
:root { --bar-h: 142px; }
}
@media (prefers-reduced-motion: reduce) {
.eq i { animation: none; height: 12px; }
* { transition-duration: 0.001ms !important; }
}(() => {
"use strict";
// --- fictional catalog (covers are CSS gradient pairs) ---
const TRACKS = [
{ title: "Paper Lanterns", artist: "Neon Tides", dur: 222, plays: "4.2M", a: "#1db954", b: "#0a6e36" },
{ title: "Velvet Static", artist: "Neon Tides", dur: 198, plays: "2.8M", a: "#8b5cf6", b: "#3b1d72" },
{ title: "Glasshouse", artist: "Marlow Vey", dur: 254, plays: "1.1M", a: "#ff3d71", b: "#7a1230" },
{ title: "Low Orbit", artist: "Cassette Bloom", dur: 176, plays: "903K", a: "#38bdf8", b: "#0b4a6e" },
{ title: "Saltwater Hymn", artist: "Neon Tides", dur: 287, plays: "6.7M", a: "#f59e0b", b: "#7a4a05" },
{ title: "Tin Roof Rain", artist: "Marlow Vey", dur: 211, plays: "512K", a: "#34d399", b: "#0f5c43" },
{ title: "Afterimage", artist: "Cassette Bloom", dur: 233, plays: "1.9M", a: "#e879f9", b: "#6b1782" },
{ title: "Midnight Reservoir", artist: "Neon Tides", dur: 305, plays: "8.1M", a: "#60a5fa", b: "#1e3a8a" },
];
const $ = (id) => document.getElementById(id);
const player = $("player");
const playBtn = $("playBtn");
const prevBtn = $("prevBtn");
const nextBtn = $("nextBtn");
const shuffleBtn = $("shuffleBtn");
const repeatBtn = $("repeatBtn");
const likeBtn = $("likeBtn");
const muteBtn = $("muteBtn");
const queueBtn = $("queueBtn");
const expandBtn = $("expandBtn");
const trackTitle = $("trackTitle");
const trackArtist = $("trackArtist");
const curTimeEl = $("curTime");
const totTimeEl = $("totTime");
const scrub = $("scrub");
const scrubFill = $("scrubFill");
const scrubThumb = $("scrubThumb");
const vol = $("vol");
const volFill = $("volFill");
const volThumb = $("volThumb");
const queueList = $("queueList");
const toastEl = $("toast");
// --- state ---
let index = 0;
let elapsed = 0; // seconds
let playing = false;
let timer = null;
let last = 0;
let volume = 0.8;
let prevVol = 0.8;
let muted = false;
let liked = new Set();
let shuffle = false;
let repeat = false; // repeat current track
const fmt = (s) => {
s = Math.max(0, Math.round(s));
const m = Math.floor(s / 60);
const r = s % 60;
return `${m}:${r < 10 ? "0" : ""}${r}`;
};
let toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 1700);
}
// --- queue rendering ---
function buildQueue() {
queueList.innerHTML = "";
TRACKS.forEach((t, i) => {
const li = document.createElement("li");
li.className = "q-row" + (i === index ? " is-current" : "");
li.style.setProperty("--ca", t.a);
li.tabIndex = 0;
li.setAttribute("role", "button");
li.setAttribute("aria-label", `Play ${t.title} by ${t.artist}`);
li.innerHTML =
`<span class="q-row__num">${i + 1}</span>` +
`<span class="q-row__cover" style="--ca:${t.a}"></span>` +
`<span class="q-row__meta"><span class="q-row__t">${t.title}</span>` +
`<span class="q-row__a">${t.artist}</span></span>` +
`<span class="q-row__plays">${t.plays} plays</span>` +
`<span class="q-row__dur">${fmt(t.dur)}</span>`;
const go = () => { load(i); play(); };
li.addEventListener("click", go);
li.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); go(); }
});
queueList.appendChild(li);
});
}
function markCurrent() {
[...queueList.children].forEach((li, i) =>
li.classList.toggle("is-current", i === index)
);
}
// --- load a track ---
function load(i) {
index = (i + TRACKS.length) % TRACKS.length;
const t = TRACKS[index];
elapsed = 0;
trackTitle.textContent = t.title;
trackArtist.textContent = t.artist;
totTimeEl.textContent = fmt(t.dur);
player.style.setProperty("--cover-a", t.a);
player.style.setProperty("--cover-b", t.b);
likeBtn.setAttribute("aria-pressed", liked.has(index) ? "true" : "false");
renderProgress();
markCurrent();
}
function renderProgress() {
const t = TRACKS[index];
const pct = Math.min(100, (elapsed / t.dur) * 100);
scrubFill.style.width = pct + "%";
scrubThumb.style.left = pct + "%";
curTimeEl.textContent = fmt(elapsed);
scrub.setAttribute("aria-valuenow", Math.round(pct));
scrub.setAttribute("aria-valuetext", `${fmt(elapsed)} of ${fmt(t.dur)}`);
}
// --- playback loop (rAF, scaled by volume-independent real time) ---
function tick(now) {
if (!playing) return;
const dt = (now - last) / 1000;
last = now;
elapsed += dt;
const t = TRACKS[index];
if (elapsed >= t.dur) {
if (repeat) {
elapsed = 0;
} else {
next(true);
return;
}
}
renderProgress();
timer = requestAnimationFrame(tick);
}
function play() {
if (playing) return;
playing = true;
player.classList.add("is-playing");
playBtn.setAttribute("aria-pressed", "true");
playBtn.setAttribute("aria-label", "Pause");
last = performance.now();
timer = requestAnimationFrame(tick);
}
function pause() {
if (!playing) return;
playing = false;
player.classList.remove("is-playing");
playBtn.setAttribute("aria-pressed", "false");
playBtn.setAttribute("aria-label", "Play");
cancelAnimationFrame(timer);
}
function toggle() {
playing ? pause() : play();
}
function next(auto) {
const wasPlaying = playing || auto;
let i;
if (shuffle) {
do { i = Math.floor(Math.random() * TRACKS.length); }
while (TRACKS.length > 1 && i === index);
} else {
i = index + 1;
}
pause();
load(i);
if (wasPlaying) play();
}
function prev() {
const wasPlaying = playing;
// restart current if >3s in, else previous track
if (elapsed > 3) {
elapsed = 0;
renderProgress();
return;
}
pause();
load(index - 1);
if (wasPlaying) play();
}
// --- volume ---
function applyVolume() {
const v = muted ? 0 : volume;
volFill.style.width = v * 100 + "%";
volThumb.style.left = v * 100 + "%";
vol.setAttribute("aria-valuenow", Math.round(v * 100));
vol.setAttribute("aria-valuetext", `${Math.round(v * 100)} percent`);
muteBtn.setAttribute("aria-pressed", v === 0 ? "true" : "false");
muteBtn.classList.toggle("is-muted", v === 0);
muteBtn.querySelector(".i-vol").style.display = v === 0 ? "none" : "";
muteBtn.querySelector(".i-mute").style.display = v === 0 ? "" : "none";
}
function setVolume(v) {
volume = Math.min(1, Math.max(0, v));
muted = volume === 0;
if (!muted) prevVol = volume;
applyVolume();
}
// --- generic horizontal slider drag (pointer) ---
function dragSlider(el, onMove) {
function pct(e) {
const r = el.getBoundingClientRect();
return Math.min(1, Math.max(0, (e.clientX - r.left) / r.width));
}
el.addEventListener("pointerdown", (e) => {
el.setPointerCapture(e.pointerId);
el.classList.add("is-drag");
onMove(pct(e), false);
const move = (ev) => onMove(pct(ev), false);
const up = (ev) => {
onMove(pct(ev), true);
el.classList.remove("is-drag");
el.removeEventListener("pointermove", move);
el.removeEventListener("pointerup", up);
};
el.addEventListener("pointermove", move);
el.addEventListener("pointerup", up);
});
}
// --- wire scrubber ---
dragSlider(scrub, (p, done) => {
elapsed = p * TRACKS[index].dur;
renderProgress();
if (done) toast(`Seek · ${fmt(elapsed)}`);
});
scrub.addEventListener("keydown", (e) => {
const t = TRACKS[index];
let handled = true;
if (e.key === "ArrowRight") elapsed = Math.min(t.dur, elapsed + 5);
else if (e.key === "ArrowLeft") elapsed = Math.max(0, elapsed - 5);
else if (e.key === "Home") elapsed = 0;
else if (e.key === "End") elapsed = t.dur - 1;
else handled = false;
if (handled) { e.preventDefault(); renderProgress(); }
});
// --- wire volume ---
dragSlider(vol, (p) => setVolume(p));
vol.addEventListener("keydown", (e) => {
let handled = true;
if (e.key === "ArrowRight" || e.key === "ArrowUp") setVolume(volume + 0.05);
else if (e.key === "ArrowLeft" || e.key === "ArrowDown") setVolume(volume - 0.05);
else if (e.key === "Home") setVolume(0);
else if (e.key === "End") setVolume(1);
else handled = false;
if (handled) e.preventDefault();
});
// --- buttons ---
playBtn.addEventListener("click", toggle);
nextBtn.addEventListener("click", () => next(false));
prevBtn.addEventListener("click", prev);
shuffleBtn.addEventListener("click", () => {
shuffle = !shuffle;
shuffleBtn.setAttribute("aria-pressed", String(shuffle));
toast(shuffle ? "Shuffle on" : "Shuffle off");
});
repeatBtn.addEventListener("click", () => {
repeat = !repeat;
repeatBtn.setAttribute("aria-pressed", String(repeat));
toast(repeat ? "Repeat track" : "Repeat off");
});
likeBtn.addEventListener("click", () => {
const on = !liked.has(index);
on ? liked.add(index) : liked.delete(index);
likeBtn.setAttribute("aria-pressed", String(on));
toast(on ? `Saved “${TRACKS[index].title}”` : "Removed from Liked Songs");
});
muteBtn.addEventListener("click", () => {
if (muted || volume === 0) {
setVolume(prevVol || 0.5);
toast("Sound on");
} else {
prevVol = volume;
setVolume(0);
toast("Muted");
}
});
queueBtn.addEventListener("click", () => {
const on = queueBtn.getAttribute("aria-pressed") !== "true";
queueBtn.setAttribute("aria-pressed", String(on));
document.querySelector(".queue").style.display = on ? "" : "none";
toast(on ? "Queue shown" : "Queue hidden");
});
expandBtn.addEventListener("click", () => {
const el = document.documentElement;
if (!document.fullscreenElement && el.requestFullscreen) {
el.requestFullscreen().catch(() => toast("Full screen unavailable"));
} else if (document.fullscreenElement) {
document.exitFullscreen();
} else {
toast("Full screen unavailable");
}
});
// --- global keyboard shortcuts ---
document.addEventListener("keydown", (e) => {
const tag = (e.target.tagName || "").toLowerCase();
const isControl = e.target === scrub || e.target === vol;
if (e.code === "Space" && !isControl && tag !== "button") {
e.preventDefault();
toggle();
} else if (e.key === "m" && tag !== "input") {
muteBtn.click();
}
});
// --- init ---
buildQueue();
load(0);
applyVolume();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Music — Sticky Bottom Player Bar</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=Sora:wght@500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Page content (scrollable behind the sticky bar) -->
<main class="stage">
<header class="stage__head">
<span class="eyebrow">Now playing from</span>
<h1>Midnight Reservoir</h1>
<p class="byline">An album by <strong>Neon Tides</strong> · 2026 · Synthpop</p>
</header>
<section class="queue" aria-label="Up next">
<h2 class="queue__title">Up next in queue</h2>
<ol class="queue__list" id="queueList"></ol>
</section>
</main>
<!-- Sticky bottom player bar -->
<div
class="player"
id="player"
role="region"
aria-label="Now playing player"
style="--cover-a: var(--accent); --cover-b: var(--accent-2)"
>
<div class="player__inner">
<!-- LEFT: cover + meta + like -->
<div class="now" id="now">
<div class="now__cover" aria-hidden="true">
<div class="cover-art">
<span class="cover-art__blob"></span>
<span class="cover-art__blob cover-art__blob--2"></span>
<span class="cover-art__ring"></span>
</div>
<div class="eq" id="eq" aria-hidden="true">
<i></i><i></i><i></i><i></i>
</div>
</div>
<div class="now__meta">
<div class="now__title" id="trackTitle">Paper Lanterns</div>
<div class="now__artist" id="trackArtist">Neon Tides</div>
</div>
<button
class="icon-btn like"
id="likeBtn"
type="button"
aria-pressed="false"
aria-label="Save to your library"
title="Like"
>
<svg viewBox="0 0 24 24" class="i-heart" aria-hidden="true">
<path d="M12 21s-7.5-4.6-10-9.1C.3 8.6 1.7 5 5.2 5c2 0 3.3 1.1 4.1 2.4C10.2 6.1 11.5 5 13.5 5 17 5 18.4 8.6 16.7 11.9 14.2 16.4 12 21 12 21z"/>
</svg>
</button>
</div>
<!-- CENTER: transport + scrubber -->
<div class="transport">
<div class="transport__btns">
<button class="icon-btn t-toggle" id="shuffleBtn" type="button" aria-pressed="false" aria-label="Shuffle" title="Shuffle">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 3h5v5M4 20 21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/></svg>
</button>
<button class="icon-btn" id="prevBtn" type="button" aria-label="Previous track" title="Previous">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 20 9 12l10-8v16zM5 4v16"/></svg>
</button>
<button class="play-btn" id="playBtn" type="button" aria-pressed="false" aria-label="Play">
<svg class="i-play" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4.5v15l13-7.5z"/></svg>
<svg class="i-pause" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4.5h4v15H7zM13 4.5h4v15h-4z"/></svg>
</button>
<button class="icon-btn" id="nextBtn" type="button" aria-label="Next track" title="Next">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4l10 8-10 8V4zM19 4v16"/></svg>
</button>
<button class="icon-btn t-toggle" id="repeatBtn" type="button" aria-pressed="false" aria-label="Repeat" title="Repeat">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 2l4 4-4 4M3 11V9a4 4 0 0 1 4-4h14M7 22l-4-4 4-4M21 13v2a4 4 0 0 1-4 4H3"/></svg>
</button>
</div>
<div class="scrub">
<span class="scrub__time" id="curTime">0:00</span>
<div
class="scrub__bar"
id="scrub"
role="slider"
tabindex="0"
aria-label="Seek"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="0"
aria-valuetext="0:00 of 0:00"
>
<div class="scrub__fill" id="scrubFill"></div>
<div class="scrub__thumb" id="scrubThumb"></div>
</div>
<span class="scrub__time" id="totTime">0:00</span>
</div>
</div>
<!-- RIGHT: volume + queue + expand -->
<div class="extras">
<button class="icon-btn" id="muteBtn" type="button" aria-pressed="false" aria-label="Mute" title="Mute">
<svg class="i-vol" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9v6h4l5 4V5L8 9H4zM16 9a3 3 0 0 1 0 6M18.5 7a6 6 0 0 1 0 10"/></svg>
<svg class="i-mute" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9v6h4l5 4V5L8 9H4zM16 9l5 6M21 9l-5 6"/></svg>
</button>
<div
class="vol"
id="vol"
role="slider"
tabindex="0"
aria-label="Volume"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="80"
aria-valuetext="80 percent"
>
<div class="vol__fill" id="volFill"></div>
<div class="vol__thumb" id="volThumb"></div>
</div>
<button class="icon-btn t-toggle" id="queueBtn" type="button" aria-pressed="true" aria-label="Toggle queue" title="Queue">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6h13M3 12h13M3 18h9M18 14v6M18 14l3 2-3 2"/></svg>
</button>
<button class="icon-btn" id="expandBtn" type="button" aria-label="Expand to full screen" title="Full screen">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 3H3v5M16 3h5v5M21 16v5h-5M3 16v5h5"/></svg>
</button>
</div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Sticky Bottom Player Bar
A glassy, dark-first player bar pinned to the bottom of the viewport, modeled on the now-playing strip you’d find in a streaming app. The left cluster shows a small CSS-drawn album cover (gradients and shapes, no images), the track title and artist, and a heart toggle that saves the song. While playback is “on”, a tiny equalizer animates over the cover so the active state reads at a glance. The bar pulls its accent color from each track’s cover, so the scrubber, toggles, and current queue row all re-theme as the song changes.
The center holds the full transport row — shuffle, previous, a morphing play/pause button, next, and repeat — above a draggable progress scrubber with live current and total timestamps. Playback is simulated with a requestAnimationFrame loop: the fill and thumb advance, time labels update, and the track auto-advances (or repeats) when it ends. The scrubber supports click, pointer-drag, and arrow-key seeking. On the right, a volume slider with a mute morph button, a queue toggle, and a full-screen expand button round out the controls.
Everything is keyboard-usable: the scrubber and volume control expose role="slider" with aria-valuenow/aria-valuetext and respond to arrow, Home, and End keys; play, like, shuffle, repeat, and mute carry aria-pressed. Space toggles playback and m mutes. A small toast() helper surfaces feedback for seeks, likes, and mode changes, and the layout collapses gracefully down to ~360px.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.