Music — Up-Next Queue Panel
A polished up-next queue side panel for a music player. The header pins the now-playing track with an animated equalizer, a play and pause morph button, and a draggable scrubber that shows live duration timestamps. Below it, the next-in-queue list shows cover art, title, artist, and length per row, each reorderable by drag-and-drop or up and down buttons, removable with an x, plus a clear-queue action, a playlist source label, and themed accents pulled from each cover.
MCP
Codice
:root {
--bg: #0b0b0f;
--bg-2: #13131a;
--surface: #1a1a22;
--surface-2: #22222c;
--text: #f4f4f7;
--muted: #a0a0ad;
--line: rgba(255, 255, 255, 0.10);
--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 -20px rgba(0, 0, 0, 0.7);
--theme: var(--accent);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
background:
radial-gradient(900px 600px at 80% -10%, rgba(139, 92, 246, 0.16), transparent 60%),
radial-gradient(700px 500px at -10% 110%, rgba(29, 185, 84, 0.12), transparent 55%),
var(--bg);
color: var(--text);
font-family: "Inter", system-ui, 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 16px;
}
/* ---------- PANEL ---------- */
.panel {
width: 100%;
max-width: 420px;
background: linear-gradient(180deg, var(--bg-2), var(--surface));
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
overflow: hidden;
transition: --theme 0.4s;
}
.panel__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 20px 14px;
}
.panel__eyebrow {
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--theme);
font-weight: 700;
}
.panel__title h1 {
margin: 2px 0 0;
font-family: "Space Grotesk", sans-serif;
font-size: 26px;
font-weight: 700;
letter-spacing: -0.01em;
}
.icon-btn {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: var(--r-full);
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--muted);
cursor: pointer;
transition: 0.18s;
}
.icon-btn:hover { color: var(--text); border-color: var(--line-2); }
.icon-btn svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
/* ---------- COVER ART ---------- */
.cover {
border-radius: var(--r-md);
position: relative;
overflow: hidden;
flex: none;
background: var(--surface-2);
}
.cover::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(120% 90% at 20% 10%, rgba(255, 255, 255, 0.32), transparent 55%),
var(--c1, var(--accent));
}
.cover::after {
content: "";
position: absolute;
inset: -20% -10% auto auto;
width: 90%;
height: 90%;
border-radius: 50%;
background: var(--c2, var(--accent-2));
mix-blend-mode: screen;
filter: blur(2px);
opacity: 0.85;
}
.cover--lg { width: 76px; height: 76px; border-radius: var(--r-md); }
.cover--sm { width: 46px; height: 46px; border-radius: 10px; }
[data-art="0"] { --c1: #1db954; --c2: #0a6e3a; }
[data-art="1"] { --c1: #8b5cf6; --c2: #ff3d71; }
[data-art="2"] { --c1: #ff3d71; --c2: #ffae42; }
[data-art="3"] { --c1: #2dd4bf; --c2: #6366f1; }
[data-art="4"] { --c1: #f472b6; --c2: #7c3aed; }
[data-art="5"] { --c1: #38bdf8; --c2: #1db954; }
/* ---------- SECTION LABELS ---------- */
.section-label {
margin: 0 0 12px;
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
}
.section-label em {
font-style: normal;
display: inline-grid;
place-items: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: var(--r-full);
background: color-mix(in srgb, var(--theme) 22%, transparent);
color: var(--theme);
font-size: 11px;
}
/* ---------- NOW PLAYING ---------- */
.now {
padding: 6px 20px 20px;
border-bottom: 1px solid var(--line);
}
.now__card {
display: flex;
align-items: center;
gap: 14px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px;
}
.now__meta { flex: 1; min-width: 0; }
.now__title {
font-family: "Space Grotesk", sans-serif;
font-weight: 600;
font-size: 17px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.now__artist {
color: var(--muted);
font-size: 13px;
font-weight: 500;
}
/* equalizer */
.now__eq {
display: flex;
align-items: flex-end;
gap: 3px;
height: 14px;
margin-top: 8px;
}
.now__eq span {
width: 3px;
border-radius: 2px;
background: var(--theme);
animation: eq 0.9s ease-in-out infinite;
}
.now__eq span:nth-child(1) { height: 40%; animation-delay: -0.1s; }
.now__eq span:nth-child(2) { height: 90%; animation-delay: -0.4s; }
.now__eq span:nth-child(3) { height: 60%; animation-delay: -0.2s; }
.now__eq span:nth-child(4) { height: 100%; animation-delay: -0.6s; }
.now__eq span:nth-child(5) { height: 50%; animation-delay: -0.3s; }
.paused .now__eq span { animation-play-state: paused; }
@keyframes eq { 0%, 100% { transform: scaleY(0.35); } 50% { transform: scaleY(1); } }
/* play/pause morph */
.play-morph {
flex: none;
width: 48px;
height: 48px;
border-radius: var(--r-full);
border: none;
cursor: pointer;
display: grid;
place-items: center;
background: var(--theme);
color: #06140c;
box-shadow: 0 8px 22px -8px color-mix(in srgb, var(--theme) 80%, transparent);
transition: transform 0.15s, filter 0.15s;
}
.play-morph:hover { transform: scale(1.06); filter: brightness(1.08); }
.play-morph:active { transform: scale(0.95); }
.play-morph svg { width: 22px; height: 22px; fill: currentColor; }
.play-morph .ic-play { display: none; }
.play-morph .ic-pause { display: block; }
.paused .play-morph .ic-play { display: block; }
.paused .play-morph .ic-pause { display: none; }
/* scrubber */
.scrub {
display: flex;
align-items: center;
gap: 10px;
margin-top: 14px;
}
.scrub__time {
font-size: 11px;
color: var(--muted);
font-variant-numeric: tabular-nums;
width: 34px;
flex: none;
}
.scrub__time:last-child { text-align: right; }
.scrub__bar {
position: relative;
flex: 1;
height: 6px;
border-radius: var(--r-full);
background: var(--surface-2);
cursor: pointer;
}
.scrub__bar:focus-visible { outline: 2px solid var(--theme); outline-offset: 4px; }
.scrub__fill {
position: absolute;
inset: 0 auto 0 0;
width: 0%;
border-radius: var(--r-full);
background: linear-gradient(90deg, var(--theme), color-mix(in srgb, var(--theme) 50%, var(--accent-2)));
}
.scrub__knob {
position: absolute;
top: 50%;
left: 0%;
width: 13px;
height: 13px;
border-radius: 50%;
background: var(--text);
transform: translate(-50%, -50%);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme) 40%, transparent);
opacity: 0;
transition: opacity 0.15s;
}
.scrub__bar:hover .scrub__knob,
.scrub__bar:focus-visible .scrub__knob { opacity: 1; }
/* ---------- QUEUE ---------- */
.queue { padding: 18px 20px 20px; }
.queue__head {
display: flex;
align-items: center;
justify-content: space-between;
}
.text-btn {
background: none;
border: none;
color: var(--muted);
font-size: 12px;
font-weight: 600;
cursor: pointer;
padding: 4px 2px;
transition: color 0.15s;
}
.text-btn:hover { color: var(--accent-3); }
.source {
margin: 2px 0 14px;
font-size: 12px;
color: var(--muted);
}
.source strong { color: var(--text); font-weight: 600; }
.tracks {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.track {
display: grid;
grid-template-columns: 18px auto 1fr auto auto;
align-items: center;
gap: 11px;
padding: 8px 8px 8px 4px;
border-radius: var(--r-md);
border: 1px solid transparent;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, opacity 0.15s, transform 0.12s;
}
.track:hover { background: var(--surface); border-color: var(--line); }
.track:focus-visible { outline: 2px solid var(--theme); outline-offset: 2px; }
.track.dragging { opacity: 0.4; }
.track.drop-target { border-color: var(--theme); background: color-mix(in srgb, var(--theme) 10%, transparent); }
.track__grip {
color: var(--muted);
cursor: grab;
display: grid;
place-items: center;
}
.track__grip svg { width: 16px; height: 16px; fill: currentColor; opacity: 0.55; }
.track:hover .track__grip svg { opacity: 1; }
.track__meta { min-width: 0; }
.track__title {
display: block;
font-weight: 600;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.track__artist {
display: block;
color: var(--muted);
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.track__dur {
color: var(--muted);
font-size: 12px;
font-variant-numeric: tabular-nums;
}
.track__ctrl {
display: flex;
align-items: center;
gap: 2px;
opacity: 0;
transform: translateX(4px);
transition: 0.15s;
}
.track:hover .track__ctrl,
.track:focus-within .track__ctrl { opacity: 1; transform: none; }
.mini {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 8px;
border: none;
background: none;
color: var(--muted);
cursor: pointer;
transition: 0.15s;
}
.mini:hover { background: var(--surface-2); color: var(--text); }
.mini--x:hover { color: var(--accent-3); }
.mini:disabled { opacity: 0.3; cursor: default; }
.mini svg { width: 16px; height: 16px; fill: currentColor; }
.empty {
margin: 10px 0 0;
padding: 22px 14px;
text-align: center;
color: var(--muted);
font-size: 13px;
border: 1px dashed var(--line-2);
border-radius: var(--r-md);
}
/* ---------- TOAST ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 18px);
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--text);
padding: 11px 18px;
border-radius: var(--r-full);
font-size: 13px;
font-weight: 500;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 20;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- RESPONSIVE ---------- */
@media (max-width: 520px) {
.stage { padding: 16px 10px; }
.panel { border-radius: var(--r-md); }
.panel__title h1 { font-size: 22px; }
.cover--lg { width: 64px; height: 64px; }
.now__card { gap: 11px; }
.play-morph { width: 44px; height: 44px; }
.track__ctrl { opacity: 1; transform: none; }
.track { grid-template-columns: 18px auto 1fr auto auto; gap: 9px; }
}
@media (prefers-reduced-motion: reduce) {
.now__eq span { animation: none; }
* { transition-duration: 0.01ms !important; }
}(function () {
"use strict";
/* ---------- data ---------- */
var THEMES = ["#1db954", "#8b5cf6", "#ff3d71", "#2dd4bf", "#f472b6", "#38bdf8"];
var nowPlaying = {
title: "Paper Lanterns",
artist: "Neon Tides",
art: 0,
duration: 222 // 3:42
};
var queue = [
{ title: "Velvet Static", artist: "Halcyon Mode", art: 1, dur: 198 },
{ title: "Glass Harbor", artist: "Neon Tides", art: 2, dur: 245 },
{ title: "Reservoir Drift", artist: "Ivory Lanes", art: 3, dur: 174 },
{ title: "Slow Motion Rain", artist: "Halcyon Mode", art: 4, dur: 263 },
{ title: "Afterglow", artist: "Velour Skies", art: 5, dur: 211 }
];
/* ---------- elements ---------- */
var listEl = document.getElementById("trackList");
var tpl = document.getElementById("trackTpl");
var countEl = document.getElementById("queueCount");
var emptyEl = document.getElementById("emptyState");
var clearBtn = document.getElementById("clearBtn");
var closeBtn = document.getElementById("closeBtn");
var panel = document.querySelector(".panel");
var npTitle = document.getElementById("npTitle");
var npArtist = document.getElementById("npArtist");
var npCover = panel.querySelector(".cover--lg");
var playBtn = document.getElementById("playBtn");
var nowBlock = document.getElementById("nowPlaying");
var scrubBar = document.getElementById("scrubBar");
var scrubFill = document.getElementById("scrubFill");
var scrubKnob = document.getElementById("scrubKnob");
var curTime = document.getElementById("curTime");
var durTime = document.getElementById("durTime");
/* ---------- helpers ---------- */
function fmt(s) {
s = Math.max(0, Math.floor(s));
var m = Math.floor(s / 60);
var r = s % 60;
return m + ":" + (r < 10 ? "0" : "") + r;
}
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
function setTheme(artIndex) {
panel.style.setProperty("--theme", THEMES[artIndex % THEMES.length]);
}
/* ---------- now playing ---------- */
var playing = true;
var elapsed = 0;
var tickTimer;
function applyNowPlaying() {
npTitle.textContent = nowPlaying.title;
npArtist.textContent = nowPlaying.artist;
npCover.setAttribute("data-art", String(nowPlaying.art));
durTime.textContent = fmt(nowPlaying.duration);
scrubBar.setAttribute("aria-valuemax", String(nowPlaying.duration));
setTheme(nowPlaying.art);
renderScrub();
}
function renderScrub() {
var pct = nowPlaying.duration ? (elapsed / nowPlaying.duration) * 100 : 0;
pct = Math.min(100, Math.max(0, pct));
scrubFill.style.width = pct + "%";
scrubKnob.style.left = pct + "%";
curTime.textContent = fmt(elapsed);
scrubBar.setAttribute("aria-valuenow", String(Math.floor(elapsed)));
}
function tick() {
if (!playing) return;
elapsed += 1;
if (elapsed >= nowPlaying.duration) {
// auto-advance to next queued track
if (queue.length) {
promote(0, true);
} else {
elapsed = nowPlaying.duration;
setPlaying(false);
toast("Queue finished");
}
return;
}
renderScrub();
}
function startClock() {
clearInterval(tickTimer);
tickTimer = setInterval(tick, 1000);
}
function setPlaying(state) {
playing = state;
playBtn.setAttribute("aria-pressed", String(state));
playBtn.setAttribute("aria-label", state ? "Pause" : "Play");
panel.classList.toggle("paused", !state);
}
playBtn.addEventListener("click", function () {
setPlaying(!playing);
toast(playing ? "Playing" : "Paused");
});
/* ---------- scrubber interaction ---------- */
function seekFromClientX(clientX) {
var rect = scrubBar.getBoundingClientRect();
var ratio = (clientX - rect.left) / rect.width;
ratio = Math.min(1, Math.max(0, ratio));
elapsed = ratio * nowPlaying.duration;
renderScrub();
}
var seeking = false;
scrubBar.addEventListener("pointerdown", function (e) {
seeking = true;
scrubBar.setPointerCapture(e.pointerId);
seekFromClientX(e.clientX);
});
scrubBar.addEventListener("pointermove", function (e) {
if (seeking) seekFromClientX(e.clientX);
});
scrubBar.addEventListener("pointerup", function () { seeking = false; });
scrubBar.addEventListener("keydown", function (e) {
var step = 0;
if (e.key === "ArrowRight" || e.key === "ArrowUp") step = 5;
else if (e.key === "ArrowLeft" || e.key === "ArrowDown") step = -5;
else if (e.key === "Home") { elapsed = 0; renderScrub(); e.preventDefault(); return; }
else if (e.key === "End") { elapsed = nowPlaying.duration; renderScrub(); e.preventDefault(); return; }
else return;
elapsed = Math.min(nowPlaying.duration, Math.max(0, elapsed + step));
renderScrub();
e.preventDefault();
});
/* ---------- queue rendering ---------- */
function render() {
listEl.innerHTML = "";
queue.forEach(function (t, i) {
var node = tpl.content.firstElementChild.cloneNode(true);
node.dataset.index = String(i);
node.querySelector(".cover--sm").setAttribute("data-art", String(t.art));
node.querySelector(".track__title").textContent = t.title;
node.querySelector(".track__artist").textContent = t.artist;
node.querySelector(".track__dur").textContent = fmt(t.dur);
var up = node.querySelector('[data-act="up"]');
var down = node.querySelector('[data-act="down"]');
up.disabled = i === 0;
down.disabled = i === queue.length - 1;
listEl.appendChild(node);
});
countEl.textContent = String(queue.length);
emptyEl.hidden = queue.length > 0;
clearBtn.disabled = queue.length === 0;
clearBtn.style.opacity = queue.length === 0 ? "0.4" : "";
}
/* ---------- queue actions ---------- */
function move(from, to) {
if (to < 0 || to >= queue.length) return;
var item = queue.splice(from, 1)[0];
queue.splice(to, 0, item);
render();
}
function remove(i) {
var t = queue.splice(i, 1)[0];
render();
toast("Removed “" + t.title + "”");
}
function promote(i, isAuto) {
var t = queue.splice(i, 1)[0];
if (!t) return;
nowPlaying = { title: t.title, artist: t.artist, art: t.art, duration: t.dur };
elapsed = 0;
applyNowPlaying();
setPlaying(true);
render();
nowBlock.animate(
[{ transform: "scale(0.985)", opacity: 0.65 }, { transform: "none", opacity: 1 }],
{ duration: 260, easing: "ease-out" }
);
if (!isAuto) toast("Now playing “" + t.title + "”");
}
listEl.addEventListener("click", function (e) {
var btn = e.target.closest("button[data-act]");
var row = e.target.closest(".track");
if (!row) return;
var i = Number(row.dataset.index);
if (btn) {
var act = btn.dataset.act;
if (act === "up") move(i, i - 1);
else if (act === "down") move(i, i + 1);
else if (act === "remove") remove(i);
e.stopPropagation();
return;
}
promote(i);
});
listEl.addEventListener("keydown", function (e) {
var row = e.target.closest(".track");
if (!row) return;
if (e.target.tagName === "BUTTON") return;
if (e.key === "Enter" || e.key === " ") {
promote(Number(row.dataset.index));
e.preventDefault();
}
});
clearBtn.addEventListener("click", function () {
if (!queue.length) return;
queue = [];
render();
toast("Queue cleared");
});
closeBtn.addEventListener("click", function () {
toast("Panel closed (demo)");
});
/* ---------- drag & drop reorder ---------- */
var dragIndex = null;
listEl.addEventListener("dragstart", function (e) {
var row = e.target.closest(".track");
if (!row) return;
dragIndex = Number(row.dataset.index);
row.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
try { e.dataTransfer.setData("text/plain", String(dragIndex)); } catch (err) {}
});
listEl.addEventListener("dragend", function () {
dragIndex = null;
Array.prototype.forEach.call(listEl.children, function (c) {
c.classList.remove("dragging", "drop-target");
});
});
listEl.addEventListener("dragover", function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
var row = e.target.closest(".track");
Array.prototype.forEach.call(listEl.children, function (c) {
c.classList.remove("drop-target");
});
if (row && Number(row.dataset.index) !== dragIndex) {
row.classList.add("drop-target");
}
});
listEl.addEventListener("drop", function (e) {
e.preventDefault();
var row = e.target.closest(".track");
if (!row || dragIndex === null) return;
var target = Number(row.dataset.index);
if (target === dragIndex) return;
move(dragIndex, target);
toast("Reordered queue");
});
/* ---------- init ---------- */
applyNowPlaying();
setPlaying(true);
render();
startClock();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Up-Next Queue Panel</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=Space+Grotesk:wght@500;600;700&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="panel" aria-label="Up next queue">
<header class="panel__head">
<div class="panel__title">
<span class="panel__eyebrow">Queue</span>
<h1>Up Next</h1>
</div>
<button class="icon-btn" id="closeBtn" type="button" aria-label="Close queue panel">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>
</button>
</header>
<!-- NOW PLAYING -->
<div class="now" id="nowPlaying">
<h2 class="section-label">Now playing</h2>
<div class="now__card">
<div class="cover cover--lg" data-art="0" aria-hidden="true"></div>
<div class="now__meta">
<div class="now__title" id="npTitle">Paper Lanterns</div>
<div class="now__artist" id="npArtist">Neon Tides</div>
<div class="now__eq" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span>
</div>
</div>
<button class="play-morph" id="playBtn" type="button" aria-pressed="true" aria-label="Pause">
<svg class="ic-play" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"/></svg>
<svg class="ic-pause" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5h3v14H7zM14 5h3v14h-3z"/></svg>
</button>
</div>
<div class="scrub">
<span class="scrub__time" id="curTime">0:00</span>
<div class="scrub__bar" id="scrubBar" role="slider" tabindex="0"
aria-label="Seek" aria-valuemin="0" aria-valuemax="222" aria-valuenow="0">
<div class="scrub__fill" id="scrubFill"></div>
<div class="scrub__knob" id="scrubKnob"></div>
</div>
<span class="scrub__time" id="durTime">3:42</span>
</div>
</div>
<!-- QUEUE -->
<div class="queue">
<div class="queue__head">
<h2 class="section-label">Next in queue <em id="queueCount">5</em></h2>
<button class="text-btn" id="clearBtn" type="button">Clear queue</button>
</div>
<p class="source">From: <strong id="sourceName">Midnight Reservoir</strong></p>
<ul class="tracks" id="trackList" aria-label="Upcoming tracks"></ul>
<p class="empty" id="emptyState" hidden>Queue is empty — add something to keep the night going.</p>
</div>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<template id="trackTpl">
<li class="track" draggable="true" tabindex="0">
<span class="track__grip" aria-hidden="true">
<svg viewBox="0 0 24 24"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg>
</span>
<span class="cover cover--sm" aria-hidden="true"></span>
<span class="track__meta">
<span class="track__title"></span>
<span class="track__artist"></span>
</span>
<span class="track__dur"></span>
<span class="track__ctrl">
<button class="mini" data-act="up" type="button" aria-label="Move up">
<svg viewBox="0 0 24 24"><path d="M12 8l5 6H7z"/></svg>
</button>
<button class="mini" data-act="down" type="button" aria-label="Move down">
<svg viewBox="0 0 24 24"><path d="M12 16l-5-6h10z"/></svg>
</button>
<button class="mini mini--x" data-act="remove" type="button" aria-label="Remove from queue">
<svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg>
</button>
</span>
</li>
</template>
<script src="script.js"></script>
</body>
</html>Up-Next Queue Panel
A self-contained side panel that sits beside any music player and answers one question: what plays next? The top section pins the current track with a CSS-drawn cover, an animated equalizer, and a morphing play/pause button. A draggable progress scrubber sits underneath with live timestamps, simulated playback driven by a one-second timer, and keyboard seeking via the arrow, Home, and End keys.
The lower section is the queue itself. Each row carries its own cover art, title, artist, and duration, and exposes a drag handle plus up/down fallback buttons so order can be changed with a pointer or the keyboard. Reordering uses native drag-and-drop with a live drop-target highlight, items can be removed individually with the x button, and a Clear queue control empties the list. Clicking any queued track promotes it straight to now-playing, re-themes the panel from the cover, and resets the scrubber.
Everything is vanilla HTML, CSS, and JavaScript with no audio files — playback is simulated with timers and transforms, and a small toast() helper confirms each action. The layout collapses cleanly down to ~360px and respects reduced-motion preferences.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.