Music — Full-Screen Now Playing
A full-screen now-playing music view with a huge CSS-drawn vinyl cover, ambient color glow pulled from the artwork, and a blurred cover-art backdrop. Features a waveform progress scrubber you can drag or seek by keyboard, big transport controls for shuffle, prev, play-pause, next and repeat, plus like, add-to-playlist, share and queue actions. A sliding drawer toggles between synced lyrics and an up-next queue, all driven by simulated playback that spins the cover and recolors the glow per track.
MCP
Code
: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;
/* Themed-from-cover accent — recolored by JS */
--cover-a: #8b5cf6;
--cover-b: #ff3d71;
--cover-c: #1db954;
--cover-glow: rgba(139, 92, 246, 0.55);
--shadow-soft: 0 18px 60px rgba(0, 0, 0, 0.55);
}
*,
*::before,
*::after { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
svg { width: 100%; height: 100%; }
button svg {
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
/* Filled glyphs override stroke */
.like-btn svg, .play-btn svg, #prev svg, #next svg { stroke: none; fill: currentColor; }
/* ---------- Backdrop ---------- */
.backdrop {
position: fixed;
inset: -8%;
z-index: 0;
background:
radial-gradient(60% 50% at 25% 20%, var(--cover-a), transparent 70%),
radial-gradient(55% 55% at 80% 30%, var(--cover-b), transparent 72%),
radial-gradient(70% 60% at 50% 95%, var(--cover-c), transparent 75%),
var(--bg);
filter: blur(80px) saturate(1.2);
opacity: 0.55;
transition: background 1.1s ease, opacity 1.1s ease;
}
.vignette {
position: fixed;
inset: 0;
z-index: 1;
pointer-events: none;
background:
radial-gradient(120% 90% at 50% 0%, transparent 40%, rgba(0,0,0,0.4) 100%),
linear-gradient(180deg, rgba(11,11,15,0.2), rgba(11,11,15,0.85));
}
/* ---------- Layout ---------- */
.player {
position: relative;
z-index: 2;
min-height: 100dvh;
max-width: 520px;
margin: 0 auto;
padding: 18px 22px 28px;
display: flex;
flex-direction: column;
gap: 18px;
}
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
gap: 14px;
}
.topbar-meta {
flex: 1;
text-align: center;
display: flex;
flex-direction: column;
line-height: 1.25;
}
.topbar-label {
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.topbar-album {
font-size: 14px;
font-weight: 600;
}
.icon-btn {
width: 40px;
height: 40px;
flex: 0 0 auto;
display: grid;
place-items: center;
padding: 9px;
border: 1px solid var(--line);
border-radius: var(--r-full);
background: rgba(255, 255, 255, 0.04);
color: var(--text);
cursor: pointer;
transition: background 0.18s ease, transform 0.12s ease, border-color 0.18s ease;
}
.icon-btn:hover { background: rgba(255, 255, 255, 0.10); border-color: var(--line-2); }
.icon-btn:active { transform: scale(0.94); }
/* ---------- Stage ---------- */
.stage {
display: flex;
flex-direction: column;
gap: 20px;
flex: 1;
}
/* Cover + glow */
.cover-wrap {
position: relative;
display: grid;
place-items: center;
margin: 8px auto 4px;
width: min(72vw, 340px);
aspect-ratio: 1;
}
.glow {
position: absolute;
inset: -14%;
border-radius: 50%;
background: radial-gradient(circle, var(--cover-glow), transparent 68%);
filter: blur(34px);
opacity: 0.9;
animation: pulse 4.2s ease-in-out infinite;
transition: background 1.1s ease;
}
@keyframes pulse {
0%, 100% { transform: scale(0.96); opacity: 0.78; }
50% { transform: scale(1.06); opacity: 1; }
}
.cover {
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
box-shadow: var(--shadow-soft), 0 0 0 1px rgba(255,255,255,0.06) inset;
display: grid;
place-items: center;
animation: spin 22s linear infinite;
animation-play-state: paused;
}
.cover.spinning { animation-play-state: running; }
@keyframes spin { to { transform: rotate(360deg); } }
.cover-art {
position: absolute;
inset: 0;
border-radius: 50%;
overflow: hidden;
background:
conic-gradient(from 220deg, var(--cover-a), var(--cover-b), var(--cover-c), var(--cover-a));
transition: background 0.9s ease;
}
.cover-art::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(40% 40% at 30% 28%, rgba(255,255,255,0.35), transparent 60%),
repeating-radial-gradient(circle at 50% 50%, rgba(0,0,0,0.18) 0 3px, transparent 3px 7px);
mix-blend-mode: overlay;
}
.cover-art::after {
content: "";
position: absolute;
inset: 14%;
border-radius: 50%;
background:
linear-gradient(135deg, rgba(255,255,255,0.12), transparent 55%),
var(--cover-b);
filter: saturate(1.3);
box-shadow: 0 0 0 2px rgba(0,0,0,0.25) inset;
}
.cover-ring {
position: absolute;
inset: 30%;
border-radius: 50%;
border: 2px solid rgba(255,255,255,0.16);
}
.cover-hole {
position: absolute;
width: 14%;
height: 14%;
border-radius: 50%;
background: var(--bg);
box-shadow: 0 0 0 4px rgba(0,0,0,0.4), 0 0 0 5px rgba(255,255,255,0.08);
}
/* Floating equalizer */
.eq {
position: absolute;
bottom: -6px;
display: flex;
align-items: flex-end;
gap: 4px;
height: 26px;
padding: 6px 10px;
border-radius: var(--r-full);
background: rgba(11, 11, 15, 0.7);
border: 1px solid var(--line);
backdrop-filter: blur(8px);
opacity: 0;
transform: translateY(6px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.eq.is-on { opacity: 1; transform: translateY(0); }
.eq span {
width: 3px;
height: 6px;
border-radius: 3px;
background: var(--cover-c);
animation: bounce 0.9s ease-in-out infinite;
}
.eq span:nth-child(2) { animation-delay: 0.15s; background: var(--cover-a); }
.eq span:nth-child(3) { animation-delay: 0.3s; }
.eq span:nth-child(4) { animation-delay: 0.45s; background: var(--cover-b); }
.eq span:nth-child(5) { animation-delay: 0.6s; }
@keyframes bounce {
0%, 100% { height: 6px; }
50% { height: 20px; }
}
/* Track meta */
.track-meta {
display: flex;
align-items: center;
gap: 14px;
}
.track-text { flex: 1; min-width: 0; }
.track-title {
font-family: "Sora", "Inter", sans-serif;
font-size: clamp(26px, 7vw, 34px);
font-weight: 800;
line-height: 1.1;
margin: 0 0 4px;
letter-spacing: -0.02em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.track-artist {
margin: 0;
color: var(--muted);
font-size: 16px;
font-weight: 500;
}
.like-btn {
width: 48px;
height: 48px;
flex: 0 0 auto;
display: grid;
place-items: center;
padding: 11px;
border: 1px solid var(--line);
border-radius: var(--r-full);
background: rgba(255, 255, 255, 0.04);
color: var(--muted);
cursor: pointer;
transition: transform 0.14s ease, color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}
.like-btn:hover { border-color: var(--line-2); color: var(--text); }
.like-btn:active { transform: scale(0.9); }
.like-btn[aria-pressed="true"] {
color: var(--accent-3);
border-color: rgba(255, 61, 113, 0.5);
background: rgba(255, 61, 113, 0.12);
}
.like-btn.bump { animation: bump 0.4s ease; }
@keyframes bump {
30% { transform: scale(1.25); }
60% { transform: scale(0.92); }
}
/* Scrubber */
.scrub-block { display: flex; flex-direction: column; gap: 8px; }
.scrubber {
position: relative;
height: 44px;
cursor: pointer;
display: flex;
align-items: center;
outline: none;
}
.scrubber:focus-visible { outline: 2px solid var(--cover-a); outline-offset: 4px; border-radius: var(--r-sm); }
.wave, .wave-played {
position: absolute;
inset: 0;
display: flex;
align-items: center;
gap: 2px;
pointer-events: none;
}
.wave .bar { background: rgba(255, 255, 255, 0.16); }
.wave-played {
overflow: hidden;
width: 0%;
transition: width 0.18s linear;
}
.wave-played .bar { background: var(--cover-c); }
.bar {
flex: 1;
min-width: 2px;
border-radius: 2px;
}
.scrub-handle {
position: absolute;
left: 0;
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
transform: translateX(-50%);
pointer-events: none;
transition: left 0.18s linear;
}
.time-row {
display: flex;
justify-content: space-between;
}
.time {
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--muted);
font-weight: 500;
}
/* Transport */
.transport {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 4px 4px 0;
}
.t-btn {
width: 50px;
height: 50px;
display: grid;
place-items: center;
padding: 13px;
border: none;
border-radius: var(--r-full);
background: transparent;
color: var(--text);
cursor: pointer;
transition: transform 0.14s ease, color 0.2s ease, background 0.2s ease;
}
.t-btn:hover { background: rgba(255,255,255,0.06); }
.t-btn:active { transform: scale(0.9); }
#shuffle, #repeat { color: var(--muted); }
.t-btn[aria-pressed="true"] { color: var(--cover-c); position: relative; }
.t-btn[aria-pressed="true"]::after {
content: "";
position: absolute;
bottom: 7px;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--cover-c);
}
.play-btn {
position: relative;
width: 72px;
height: 72px;
flex: 0 0 auto;
display: grid;
place-items: center;
border: none;
border-radius: 50%;
background: var(--text);
color: var(--bg);
cursor: pointer;
box-shadow: 0 12px 30px rgba(0,0,0,0.45);
transition: transform 0.14s ease, box-shadow 0.2s ease;
}
.play-btn:hover { transform: scale(1.05); }
.play-btn:active { transform: scale(0.95); }
.play-btn svg {
position: absolute;
width: 30px;
height: 30px;
transition: opacity 0.2s ease, transform 0.25s ease;
}
.play-btn .ic-play { transform: translateX(1px); }
.play-btn .ic-pause { opacity: 0; transform: scale(0.6); }
.play-btn[aria-pressed="true"] .ic-play { opacity: 0; transform: scale(0.6); }
.play-btn[aria-pressed="true"] .ic-pause { opacity: 1; transform: scale(1); }
/* Secondary actions */
.actions {
display: flex;
justify-content: space-between;
gap: 8px;
}
.a-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
padding: 10px 4px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: rgba(255, 255, 255, 0.03);
color: var(--muted);
cursor: pointer;
font-family: inherit;
font-size: 11px;
font-weight: 600;
transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease, transform 0.12s ease;
}
.a-btn svg { width: 18px; height: 18px; }
.a-btn:hover { color: var(--text); background: rgba(255,255,255,0.07); border-color: var(--line-2); }
.a-btn:active { transform: scale(0.96); }
.a-btn[aria-expanded="true"] { color: var(--cover-c); border-color: rgba(255,255,255,0.22); }
/* ---------- Drawer ---------- */
.drawer {
position: fixed;
left: 50%;
bottom: 0;
transform: translate(-50%, calc(100% - 26px));
width: min(100%, 520px);
max-height: 78dvh;
z-index: 5;
background: var(--surface);
border: 1px solid var(--line);
border-bottom: none;
border-radius: var(--r-lg) var(--r-lg) 0 0;
box-shadow: 0 -20px 60px rgba(0,0,0,0.55);
display: flex;
flex-direction: column;
transition: transform 0.4s cubic-bezier(0.22, 1, 0.36, 1);
}
.drawer.is-open { transform: translate(-50%, 0); }
.drawer-handle {
margin: 10px auto 4px;
width: 44px;
height: 5px;
border-radius: var(--r-full);
background: var(--line-2);
cursor: pointer;
flex: 0 0 auto;
}
.drawer-tabs {
display: flex;
gap: 6px;
padding: 8px 18px 12px;
border-bottom: 1px solid var(--line);
}
.d-tab {
padding: 8px 16px;
border: none;
border-radius: var(--r-full);
background: transparent;
color: var(--muted);
font-family: inherit;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease;
}
.d-tab:hover { color: var(--text); }
.d-tab.is-active {
background: var(--surface-2);
color: var(--text);
}
.drawer-panel {
overflow-y: auto;
padding: 8px 12px 22px;
}
.drawer-panel.is-hidden { display: none; }
.queue { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.q-item {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 10px;
border-radius: var(--r-md);
cursor: pointer;
transition: background 0.16s ease;
}
.q-item:hover { background: var(--surface-2); }
.q-item.is-current { background: rgba(255,255,255,0.05); }
.q-thumb {
width: 44px;
height: 44px;
flex: 0 0 auto;
border-radius: var(--r-sm);
background: conic-gradient(from 200deg, var(--qa), var(--qb), var(--qa));
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
.q-info { flex: 1; min-width: 0; }
.q-title {
font-size: 14px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.q-sub {
font-size: 12px;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.q-dur {
font-size: 12px;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.q-item.is-current .q-dur {
color: var(--cover-c);
display: flex;
align-items: flex-end;
gap: 2px;
height: 12px;
}
.q-item.is-current .q-dur::before,
.q-item.is-current .q-dur::after {
content: "";
width: 3px;
background: var(--cover-c);
border-radius: 2px;
animation: bounce 0.8s ease-in-out infinite;
}
.q-item.is-current .q-dur::after { animation-delay: 0.3s; }
/* Lyrics */
.lyrics {
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px 10px;
}
.lyric-line {
font-size: 18px;
font-weight: 600;
color: var(--muted);
padding: 5px 0;
transition: color 0.3s ease, transform 0.3s ease;
}
.lyric-line.is-active {
color: var(--text);
transform: translateX(4px);
}
.lyric-line.is-active::before {
content: "";
display: inline-block;
width: 6px;
height: 6px;
margin-right: 10px;
border-radius: 50%;
background: var(--cover-c);
vertical-align: middle;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 40px;
transform: translate(-50%, 20px);
z-index: 20;
padding: 11px 18px;
border-radius: var(--r-full);
background: rgba(20, 20, 26, 0.92);
border: 1px solid var(--line-2);
color: var(--text);
font-size: 13px;
font-weight: 600;
backdrop-filter: blur(10px);
box-shadow: var(--shadow-soft);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.player { padding: 14px 16px 24px; }
.cover-wrap { width: min(76vw, 300px); }
.play-btn { width: 66px; height: 66px; }
.t-btn { width: 46px; height: 46px; }
.a-btn span { font-size: 10px; }
.track-title { font-size: clamp(24px, 8vw, 30px); }
}
@media (max-width: 360px) {
.actions { gap: 6px; }
.a-btn { padding: 9px 2px; }
.transport { gap: 4px; }
}
@media (prefers-reduced-motion: reduce) {
.cover, .glow, .eq span, .q-item.is-current .q-dur::before, .q-item.is-current .q-dur::after {
animation: none !important;
}
}(function () {
"use strict";
/* ---------- Fictional catalog ---------- */
var TRACKS = [
{
title: "Paper Lanterns",
artist: "Neon Tides",
album: "Midnight Reservoir",
duration: 222, // 3:42
colors: ["#8b5cf6", "#ff3d71", "#1db954"],
glow: "rgba(139, 92, 246, 0.55)",
lyrics: [
"Paper lanterns on the wire,",
"flickering like a slow desire,",
"we drift below the humming light,",
"two shadows folding into night.",
"Hold the quiet, let it stay —",
"the reservoir will drink the day.",
"And every echo that we made",
"dissolves into the river's shade."
]
},
{
title: "Velvet Static",
artist: "Cassette Bloom",
album: "Saltwater Cassette",
duration: 198, // 3:18
colors: ["#ff3d71", "#ffb13d", "#8b5cf6"],
glow: "rgba(255, 61, 113, 0.5)",
lyrics: [
"Velvet static on the line,",
"your voice arriving out of time,",
"I tune the dial to find your face",
"in the warm hiss of empty space.",
"Stay a little, don't sign off —",
"the signal's soft, the night is long.",
"We are the noise between the songs,",
"the part that fades but lingers on."
]
},
{
title: "Glass Avenue",
artist: "Aurora Lowtide",
album: "Northbound Aurora",
duration: 254, // 4:14
colors: ["#1db954", "#34d8ff", "#8b5cf6"],
glow: "rgba(52, 216, 255, 0.5)",
lyrics: [
"Down on Glass Avenue tonight,",
"the rain rewrites the city lights,",
"I count the windows, one by one,",
"small constellations, never done.",
"Carry me past the neon haze,",
"the northbound train, the borrowed days.",
"And if the morning finds us here,",
"we'll call it home, we'll disappear."
]
},
{
title: "Slow Comet",
artist: "Marble Heights",
album: "Orbit of Small Things",
duration: 176, // 2:56
colors: ["#ffb13d", "#ff3d71", "#1db954"],
glow: "rgba(255, 177, 61, 0.5)",
lyrics: [
"Slow comet over rooftop tar,",
"you wished on something just as far,",
"a tail of dust, a borrowed flame,",
"you whispered out a stranger's name.",
"Let it burn, let it go —",
"the orbit pulls us, soft and slow.",
"We are the small and falling things",
"that learn to fly without the wings."
]
},
{
title: "Harbor Lights",
artist: "Quiet Atlas",
album: "Coast & Current",
duration: 231, // 3:51
colors: ["#34d8ff", "#8b5cf6", "#1db954"],
glow: "rgba(52, 216, 255, 0.55)",
lyrics: [
"Harbor lights in amber rows,",
"the tide remembers where it goes,",
"I leave my worries on the pier",
"and let the current pull them clear.",
"Steady now, the morning breaks,",
"across the coast the silver wakes.",
"Whatever sank will surface soon —",
"the harbor keeps a softer tune."
]
}
];
/* ---------- DOM ---------- */
var $ = function (id) { return document.getElementById(id); };
var root = document.documentElement;
var backdrop = $("backdrop");
var cover = $("cover");
var glow = $("glow");
var eq = $("eq");
var trackTitle = $("trackTitle");
var trackArtist = $("trackArtist");
var topbarAlbum = $("topbarAlbum");
var playBtn = $("play");
var likeBtn = $("like");
var shuffleBtn = $("shuffle");
var repeatBtn = $("repeat");
var prevBtn = $("prev");
var nextBtn = $("next");
var scrubber = $("scrubber");
var wave = $("wave");
var wavePlayed = $("wavePlayed");
var scrubHandle = $("scrubHandle");
var timeCurrent = $("timeCurrent");
var timeTotal = $("timeTotal");
var drawer = $("drawer");
var drawerHandle = $("drawerHandle");
var queueToggle = $("queueToggle");
var queueEl = $("queue");
var lyricsEl = $("lyrics");
var toastEl = $("toast");
/* ---------- State ---------- */
var current = 0;
var position = 0; // seconds
var playing = false;
var liked = {}; // index -> bool
var repeatOn = false;
var shuffleOn = false;
var timer = null;
var WAVE_BARS = 64;
/* ---------- Helpers ---------- */
function fmt(sec) {
sec = Math.max(0, Math.floor(sec));
var m = Math.floor(sec / 60);
var s = sec % 60;
return m + ":" + (s < 10 ? "0" + s : s);
}
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 1900);
}
/* Build static waveform bars (seeded so each track has its own shape) */
function buildWave(seed) {
var htmlA = "", htmlB = "";
var x = seed * 9301 + 49297;
function rnd() { x = (x * 9301 + 49297) % 233280; return x / 233280; }
for (var i = 0; i < WAVE_BARS; i++) {
var base = 0.25 + Math.abs(Math.sin(i * 0.4 + seed)) * 0.55;
var h = Math.round((base + rnd() * 0.2) * 100);
if (h > 100) h = 100;
var bar = '<span class="bar" style="height:' + h + '%"></span>';
htmlA += bar;
htmlB += bar;
}
wave.innerHTML = htmlA;
wavePlayed.innerHTML = htmlB;
}
/* ---------- Render track ---------- */
function loadTrack(i, opts) {
opts = opts || {};
current = ((i % TRACKS.length) + TRACKS.length) % TRACKS.length;
var t = TRACKS[current];
// Recolor ambient theme from "cover"
root.style.setProperty("--cover-a", t.colors[0]);
root.style.setProperty("--cover-b", t.colors[1]);
root.style.setProperty("--cover-c", t.colors[2]);
root.style.setProperty("--cover-glow", t.glow);
trackTitle.textContent = t.title;
trackArtist.textContent = t.artist;
topbarAlbum.textContent = t.album;
timeTotal.textContent = fmt(t.duration);
likeBtn.setAttribute("aria-pressed", liked[current] ? "true" : "false");
buildWave(current + 1);
renderLyrics();
renderQueue();
position = 0;
updateProgress();
if (opts.autoplay) { setPlaying(true); }
else { syncEqAndSpin(); }
}
/* ---------- Progress ---------- */
function updateProgress() {
var t = TRACKS[current];
var pct = t.duration ? (position / t.duration) * 100 : 0;
if (pct > 100) pct = 100;
wavePlayed.style.width = pct + "%";
scrubHandle.style.left = pct + "%";
timeCurrent.textContent = fmt(position);
scrubber.setAttribute("aria-valuenow", Math.round(pct));
scrubber.setAttribute("aria-valuetext", fmt(position) + " of " + fmt(t.duration));
highlightLyric(pct);
}
/* ---------- Playback engine (simulated) ---------- */
function tick() {
var t = TRACKS[current];
position += 1;
if (position >= t.duration) {
if (repeatOn) {
position = 0;
} else {
position = t.duration;
updateProgress();
gotoNext(true);
return;
}
}
updateProgress();
}
function setPlaying(on) {
playing = on;
playBtn.setAttribute("aria-pressed", on ? "true" : "false");
playBtn.setAttribute("aria-label", on ? "Pause" : "Play");
syncEqAndSpin();
clearInterval(timer);
if (on) { timer = setInterval(tick, 1000); }
}
function syncEqAndSpin() {
if (playing) {
cover.classList.add("spinning");
eq.classList.add("is-on");
} else {
cover.classList.remove("spinning");
eq.classList.remove("is-on");
}
}
function gotoNext(auto) {
var nextIndex;
if (shuffleOn) {
do { nextIndex = Math.floor(Math.random() * TRACKS.length); }
while (nextIndex === current && TRACKS.length > 1);
} else {
nextIndex = current + 1;
}
loadTrack(nextIndex, { autoplay: auto || playing });
if (!auto) { toast("Next: " + TRACKS[current].title); }
}
function gotoPrev() {
if (position > 3) { position = 0; updateProgress(); return; }
loadTrack(current - 1, { autoplay: playing });
}
/* ---------- Lyrics ---------- */
function renderLyrics() {
var t = TRACKS[current];
lyricsEl.innerHTML = t.lyrics.map(function (line) {
return '<div class="lyric-line">' + line + "</div>";
}).join("");
}
function highlightLyric(pct) {
var lines = lyricsEl.children;
if (!lines.length) return;
var idx = Math.min(lines.length - 1, Math.floor((pct / 100) * lines.length));
for (var i = 0; i < lines.length; i++) {
lines[i].classList.toggle("is-active", i === idx);
}
}
/* ---------- Queue ---------- */
function renderQueue() {
queueEl.innerHTML = "";
TRACKS.forEach(function (t, i) {
var li = document.createElement("li");
li.className = "q-item" + (i === current ? " is-current" : "");
li.setAttribute("role", "button");
li.setAttribute("tabindex", "0");
var dur = i === current ? "" : fmt(t.duration);
li.innerHTML =
'<span class="q-thumb" style="--qa:' + t.colors[0] + ';--qb:' + t.colors[1] + '"></span>' +
'<span class="q-info">' +
'<span class="q-title">' + t.title + "</span>" +
'<span class="q-sub">' + t.artist + " · " + t.album + "</span>" +
"</span>" +
'<span class="q-dur">' + dur + "</span>";
function pick() {
if (i === current) { setPlaying(!playing); return; }
loadTrack(i, { autoplay: true });
toast("Now playing · " + TRACKS[current].title);
}
li.addEventListener("click", pick);
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); pick(); }
});
queueEl.appendChild(li);
});
}
/* ---------- Scrubber (click + drag + keyboard) ---------- */
function pctFromEvent(clientX) {
var rect = scrubber.getBoundingClientRect();
var p = (clientX - rect.left) / rect.width;
return Math.max(0, Math.min(1, p));
}
function seekTo(p) {
position = Math.round(p * TRACKS[current].duration);
updateProgress();
}
var dragging = false;
function onDown(e) {
dragging = true;
var x = e.touches ? e.touches[0].clientX : e.clientX;
seekTo(pctFromEvent(x));
e.preventDefault();
}
function onMove(e) {
if (!dragging) return;
var x = e.touches ? e.touches[0].clientX : e.clientX;
seekTo(pctFromEvent(x));
}
function onUp() {
if (dragging) { dragging = false; }
}
scrubber.addEventListener("mousedown", onDown);
scrubber.addEventListener("touchstart", onDown, { passive: false });
window.addEventListener("mousemove", onMove);
window.addEventListener("touchmove", onMove, { passive: false });
window.addEventListener("mouseup", onUp);
window.addEventListener("touchend", onUp);
scrubber.addEventListener("keydown", function (e) {
var step = TRACKS[current].duration * 0.05;
if (e.key === "ArrowRight" || e.key === "ArrowUp") { position = Math.min(TRACKS[current].duration, position + step); updateProgress(); e.preventDefault(); }
else if (e.key === "ArrowLeft" || e.key === "ArrowDown") { position = Math.max(0, position - step); updateProgress(); e.preventDefault(); }
else if (e.key === "Home") { position = 0; updateProgress(); e.preventDefault(); }
else if (e.key === "End") { position = TRACKS[current].duration; updateProgress(); e.preventDefault(); }
});
/* ---------- Controls ---------- */
playBtn.addEventListener("click", function () { setPlaying(!playing); });
nextBtn.addEventListener("click", function () { gotoNext(false); });
prevBtn.addEventListener("click", gotoPrev);
likeBtn.addEventListener("click", function () {
liked[current] = !liked[current];
likeBtn.setAttribute("aria-pressed", liked[current] ? "true" : "false");
likeBtn.classList.remove("bump");
void likeBtn.offsetWidth;
likeBtn.classList.add("bump");
toast(liked[current] ? "Added to Liked Songs" : "Removed from Liked Songs");
});
shuffleBtn.addEventListener("click", function () {
shuffleOn = !shuffleOn;
shuffleBtn.setAttribute("aria-pressed", shuffleOn ? "true" : "false");
toast(shuffleOn ? "Shuffle on" : "Shuffle off");
});
repeatBtn.addEventListener("click", function () {
repeatOn = !repeatOn;
repeatBtn.setAttribute("aria-pressed", repeatOn ? "true" : "false");
toast(repeatOn ? "Repeat one" : "Repeat off");
});
$("addPlaylist").addEventListener("click", function () {
toast("Saved to “Late Night Drive”");
});
$("share").addEventListener("click", function () {
toast("Share link copied");
});
$("swap").addEventListener("click", function () {
var nextIndex;
do { nextIndex = Math.floor(Math.random() * TRACKS.length); }
while (nextIndex === current && TRACKS.length > 1);
loadTrack(nextIndex, { autoplay: true });
toast("Shuffle pick · " + TRACKS[current].title);
});
$("more").addEventListener("click", function () { toast("More options"); });
$("minimize").addEventListener("click", function () { toast("Minimized to mini-player"); });
/* ---------- Drawer ---------- */
function toggleDrawer(force) {
var open = typeof force === "boolean" ? force : !drawer.classList.contains("is-open");
drawer.classList.toggle("is-open", open);
drawer.setAttribute("aria-hidden", open ? "false" : "true");
queueToggle.setAttribute("aria-expanded", open ? "true" : "false");
}
queueToggle.addEventListener("click", function () { toggleDrawer(); switchTab("upnext"); });
drawerHandle.addEventListener("click", function () { toggleDrawer(); });
drawerHandle.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleDrawer(); }
});
function switchTab(which) {
var upnext = which === "upnext";
$("tabUpnext").classList.toggle("is-active", upnext);
$("tabLyrics").classList.toggle("is-active", !upnext);
$("tabUpnext").setAttribute("aria-selected", upnext ? "true" : "false");
$("tabLyrics").setAttribute("aria-selected", !upnext ? "true" : "false");
$("panelUpnext").classList.toggle("is-hidden", !upnext);
$("panelLyrics").classList.toggle("is-hidden", upnext);
}
$("tabUpnext").addEventListener("click", function () { switchTab("upnext"); });
$("tabLyrics").addEventListener("click", function () { switchTab("lyrics"); });
/* ---------- Global keyboard shortcuts ---------- */
document.addEventListener("keydown", function (e) {
if (e.target.closest && e.target.closest(".scrubber, button, [role=button]")) return;
if (e.key === " " || e.key === "k") { e.preventDefault(); setPlaying(!playing); }
else if (e.key === "ArrowRight" && e.shiftKey) { gotoNext(false); }
else if (e.key === "ArrowLeft" && e.shiftKey) { gotoPrev(); }
else if (e.key.toLowerCase() === "l") { likeBtn.click(); }
});
/* ---------- Init ---------- */
loadTrack(0);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Music — Full-Screen Now Playing</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>
<!-- Blurred cover-art backdrop -->
<div class="backdrop" id="backdrop" aria-hidden="true"></div>
<div class="vignette" aria-hidden="true"></div>
<main class="player" id="player">
<header class="topbar">
<button class="icon-btn" id="minimize" type="button" aria-label="Minimize player">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
</button>
<div class="topbar-meta">
<span class="topbar-label">Playing from album</span>
<strong class="topbar-album" id="topbarAlbum">Midnight Reservoir</strong>
</div>
<button class="icon-btn" id="more" type="button" aria-label="More options">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>
</button>
</header>
<section class="stage">
<!-- Ambient glow + big CSS-drawn cover -->
<div class="cover-wrap">
<div class="glow" id="glow" aria-hidden="true"></div>
<div class="cover spinning" id="cover" role="img" aria-label="Album cover artwork">
<div class="cover-art" id="coverArt"></div>
<div class="cover-ring" aria-hidden="true"></div>
<div class="cover-hole" aria-hidden="true"></div>
</div>
<div class="eq" id="eq" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span>
</div>
</div>
<div class="track-meta">
<div class="track-text">
<h1 class="track-title" id="trackTitle">Paper Lanterns</h1>
<p class="track-artist" id="trackArtist">Neon Tides</p>
</div>
<button class="like-btn" id="like" type="button" aria-pressed="false" aria-label="Like this track">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-10-9.2C-0.2 7.6 2.3 4 5.8 4 8 4 9.4 5.3 12 8c2.6-2.7 4-4 6.2-4 3.5 0 6 3.6 3.8 7.8C19.5 16.4 12 21 12 21z"/></svg>
</button>
</div>
<!-- Waveform / progress scrubber -->
<div class="scrub-block">
<div class="scrubber" id="scrubber" role="slider" tabindex="0"
aria-label="Seek track position" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div class="wave" id="wave" aria-hidden="true"></div>
<div class="wave-played" id="wavePlayed" aria-hidden="true"></div>
<div class="scrub-handle" id="scrubHandle" aria-hidden="true"></div>
</div>
<div class="time-row">
<span class="time" id="timeCurrent">0:00</span>
<span class="time" id="timeTotal">3:42</span>
</div>
</div>
<!-- Transport controls -->
<div class="transport">
<button class="t-btn" id="shuffle" type="button" aria-pressed="false" aria-label="Shuffle">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 3h5v5"/><path d="M4 20L21 3"/><path d="M21 16v5h-5"/><path d="M15 15l6 6"/><path d="M4 4l5 5"/></svg>
</button>
<button class="t-btn" id="prev" type="button" aria-label="Previous track">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 20L9 12l10-8v16z"/><rect x="5" y="4" width="2.5" height="16" rx="1"/></svg>
</button>
<button class="play-btn" id="play" type="button" aria-pressed="false" aria-label="Play">
<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"><rect x="6" y="5" width="4" height="14" rx="1.2"/><rect x="14" y="5" width="4" height="14" rx="1.2"/></svg>
</button>
<button class="t-btn" id="next" type="button" aria-label="Next track">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4l10 8-10 8V4z"/><rect x="16.5" y="4" width="2.5" height="16" rx="1"/></svg>
</button>
<button class="t-btn" id="repeat" type="button" aria-pressed="false" aria-label="Repeat">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M17 2l4 4-4 4"/><path d="M3 11V9a4 4 0 014-4h14"/><path d="M7 22l-4-4 4-4"/><path d="M21 13v2a4 4 0 01-4 4H3"/></svg>
</button>
</div>
<!-- Secondary actions -->
<div class="actions">
<button class="a-btn" id="addPlaylist" type="button" aria-label="Add to playlist">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
<span>Add</span>
</button>
<button class="a-btn" id="share" type="button" aria-label="Share track">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="18" cy="5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="19" r="2.5"/><path d="M8.2 10.8l7.6-4.6M8.2 13.2l7.6 4.6"/></svg>
<span>Share</span>
</button>
<button class="a-btn" id="swap" type="button" aria-label="Play a different track">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 0115.5-6.2L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 01-15.5 6.2L3 16"/><path d="M3 21v-5h5"/></svg>
<span>Shuffle Pick</span>
</button>
<button class="a-btn" id="queueToggle" type="button" aria-expanded="false" aria-controls="drawer" aria-label="Show up next queue">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h11M4 12h11M4 18h7"/><path d="M17 14l4 3-4 3z"/></svg>
<span>Queue</span>
</button>
</div>
</section>
<!-- Lyrics / Up-next drawer -->
<aside class="drawer" id="drawer" aria-hidden="true">
<div class="drawer-handle" id="drawerHandle" role="button" tabindex="0" aria-label="Toggle drawer"></div>
<div class="drawer-tabs" role="tablist">
<button class="d-tab is-active" id="tabUpnext" role="tab" aria-selected="true" type="button">Up Next</button>
<button class="d-tab" id="tabLyrics" role="tab" aria-selected="false" type="button">Lyrics</button>
</div>
<div class="drawer-panel" id="panelUpnext" role="tabpanel">
<ul class="queue" id="queue"></ul>
</div>
<div class="drawer-panel is-hidden" id="panelLyrics" role="tabpanel">
<div class="lyrics" id="lyrics"></div>
</div>
</aside>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Full-Screen Now Playing
An immersive, full-screen “Now Playing” screen built with nothing but HTML, CSS and vanilla JavaScript. The centerpiece is a large CSS-drawn vinyl-style album cover that slowly spins while a track plays, wrapped in a soft ambient glow whose color is pulled straight from the current cover. A heavily blurred copy of that artwork fills the backdrop, so the entire view re-themes itself every time you change songs.
Below the cover sit the track title and artist, a like toggle, and a tall waveform scrubber with live time labels. The waveform is generated per track and fills as playback advances; you can click, drag, or use the arrow keys to seek. Big transport controls cover shuffle, previous, an animated play/pause morph button, next, and repeat-one, with a secondary row for add-to-playlist, share, a “shuffle pick” that recolors the whole scene, and a queue button.
A bottom drawer slides up to reveal two tabs: an Up Next queue of fictional tracks (tap any to
play it) and a Lyrics view that highlights the current line in time with the simulated playback.
Everything is keyboard-accessible — space to play/pause, L to like, shift+arrows to skip — with
aria-pressed, role="slider", and live toasts wired throughout. No audio files are used; playback
is faked with timers and CSS transforms.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.