UI Components Medium
Audio Player
A stylish audio player with playlist support, album art display, and smooth progress tracking.
Open in Lab
MCP
vanilla-js html5 css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--audio-primary: #ec4899;
--audio-bg: #111827;
--audio-surface: #1f2937;
--audio-text: #f9fafb;
--audio-muted: #9ca3af;
}
.audio-widget {
background: var(--audio-bg);
border-radius: 24px;
overflow: hidden;
max-width: 360px;
margin: 0 auto;
font-family: "Inter", system-ui, sans-serif;
color: var(--audio-text);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.audio-card {
padding: 1.5rem;
background: linear-gradient(180deg, var(--audio-surface) 0%, var(--audio-bg) 100%);
}
.album-art {
width: 100%;
aspect-ratio: 1;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
margin-bottom: 1.5rem;
}
.album-art img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.track-info {
text-align: center;
margin-bottom: 1.5rem;
}
.track-info h3 {
font-size: 1.25rem;
margin: 0;
}
.track-info p {
color: var(--audio-muted);
font-size: 0.875rem;
margin: 0.25rem 0 0;
}
.progress-area {
position: relative;
margin-bottom: 1rem;
}
.audio-progress {
width: 100%;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
cursor: pointer;
outline: none;
}
.audio-progress::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
background: var(--audio-primary);
border-radius: 50%;
box-shadow: 0 0 10px rgba(236, 72, 153, 0.5);
}
.curr-time,
.total-time {
position: absolute;
top: 10px;
font-size: 0.75rem;
color: var(--audio-muted);
}
.curr-time {
left: 0;
}
.total-time {
right: 0;
}
.main-controls {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
margin-bottom: 1.5rem;
}
.ctrl-btn {
background: transparent;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
transition: all 0.2s;
}
.play-main {
width: 60px;
height: 60px;
background: var(--audio-primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
}
.play-main:hover {
transform: scale(1.05);
box-shadow: 0 0 20px rgba(236, 72, 153, 0.4);
}
.volume-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0 1rem;
}
.audio-volume {
flex: 1;
height: 4px;
}
.playlist-container {
max-height: 200px;
overflow-y: auto;
background: rgba(0, 0, 0, 0.2);
}
.playlist {
list-style: none;
margin: 0;
padding: 0;
}
.playlist li {
padding: 0.75rem 1.5rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
font-size: 0.875rem;
transition: background 0.2s;
}
.playlist li:hover {
background: rgba(255, 255, 255, 0.05);
}
.playlist li.active {
color: var(--audio-primary);
background: rgba(236, 72, 153, 0.1);
}const songs = [
{
name: "Sunset Dreams",
artist: "Lo-Fi Beats",
src: "https://www.bensound.com/bensound-music/bensound-asmile.mp3",
art: "https://images.unsplash.com/photo-1614613535308-eb5fbd3d2c17?w=400&h=400&fit=crop",
},
{
name: "Neon Nights",
artist: "Synthwave Pro",
src: "https://www.bensound.com/bensound-music/bensound-energy.mp3",
art: "https://images.unsplash.com/photo-1557683316-973673baf926?w=400&h=400&fit=crop",
},
{
name: "Mountain High",
artist: "Acoustic Folk",
src: "https://www.bensound.com/bensound-music/bensound-adventure.mp3",
art: "https://images.unsplash.com/photo-1501854140801-50d01698950b?w=400&h=400&fit=crop",
},
];
let songIndex = 0;
const audio = document.getElementById("main-audio");
const playBtn = document.getElementById("audio-play-pause");
const prevBtn = document.getElementById("prev-btn");
const nextBtn = document.getElementById("next-btn");
const progress = document.getElementById("audio-progress");
const volume = document.getElementById("audio-volume");
const trackName = document.getElementById("track-name");
const trackArtist = document.getElementById("track-artist");
const trackArt = document.getElementById("track-art");
const playlist = document.getElementById("playlist");
const currTimeEl = document.querySelector(".curr-time");
const totalTimeEl = document.querySelector(".total-time");
function loadSong(song) {
trackName.innerText = song.name;
trackArtist.innerText = song.artist;
trackArt.src = song.art;
audio.src = song.src;
updatePlaylistUI();
}
function updatePlaylistUI() {
playlist.querySelectorAll("li").forEach((li, index) => {
li.className = index === songIndex ? "active" : "";
});
}
function playSong() {
audio.play();
playBtn.innerText = "⏸";
}
function pauseSong() {
audio.pause();
playBtn.innerText = "▶";
}
function togglePlay() {
if (audio.paused) playSong();
else pauseSong();
}
function prevSong() {
songIndex = (songIndex - 1 + songs.length) % songs.length;
loadSong(songs[songIndex]);
playSong();
}
function nextSong() {
songIndex = (songIndex + 1) % songs.length;
loadSong(songs[songIndex]);
playSong();
}
function updateProgress(e) {
const { duration, currentTime } = e.srcElement;
const progressPercent = (currentTime / duration) * 100;
progress.value = progressPercent;
currTimeEl.innerText = formatTime(currentTime);
if (duration) totalTimeEl.innerText = formatTime(duration);
}
function formatTime(time) {
const min = Math.floor(time / 60);
const sec = Math.floor(time % 60);
return `${min}:${sec < 10 ? "0" : ""}${sec}`;
}
function setProgress() {
audio.currentTime = (progress.value / 100) * audio.duration;
}
// Build Playlist
songs.forEach((song, index) => {
const li = document.createElement("li");
li.innerText = `${song.name} - ${song.artist}`;
li.addEventListener("click", () => {
songIndex = index;
loadSong(songs[songIndex]);
playSong();
});
playlist.appendChild(li);
});
// Event Listeners
playBtn.addEventListener("click", togglePlay);
prevBtn.addEventListener("click", prevSong);
nextBtn.addEventListener("click", nextSong);
audio.addEventListener("timeupdate", updateProgress);
progress.addEventListener("input", setProgress);
volume.addEventListener("input", () => (audio.volume = volume.value));
audio.addEventListener("ended", nextSong);
// Init
loadSong(songs[songIndex]);<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Audio Player</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="audio-widget">
<div class="audio-card">
<div class="album-art">
<img id="track-art"
src="https://images.unsplash.com/photo-1614613535308-eb5fbd3d2c17?w=400&h=400&fit=crop"
alt="Album Art" />
</div>
<div class="track-info">
<h3 id="track-name">Track Name</h3>
<p id="track-artist">Artist Name</p>
</div>
<div class="audio-controls">
<div class="progress-area">
<div class="curr-time">0:00</div>
<div class="total-time">0:00</div>
<input type="range" id="audio-progress" min="0" max="100" value="0" class="audio-progress" />
</div>
<div class="main-controls">
<button id="prev-btn" class="ctrl-btn">⏮</button>
<button id="audio-play-pause" class="ctrl-btn play-main">▶</button>
<button id="next-btn" class="ctrl-btn">⏭</button>
</div>
<div class="volume-row">
<span class="vol-icon">🔈</span>
<input type="range" id="audio-volume" min="0" max="1" step="0.1" value="0.7" class="audio-volume" />
</div>
</div>
</div>
<div class="playlist-container">
<ul id="playlist" class="playlist">
<!-- Tracks added via JS -->
</ul>
</div>
<audio id="main-audio" src=""></audio>
</div>
<script src="script.js"></script>
</body>
</html>import { useRef, useState } from "react";
function formatTime(s: number) {
if (!isFinite(s)) return "0:00";
return `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
}
const PLAYLIST = [
{ title: "Midnight Drive", artist: "Lo-fi Collective", duration: "3:42", color: "#bc8cff" },
{ title: "Rain & Coffee", artist: "Chill Vibes", duration: "4:15", color: "#58a6ff" },
{ title: "Golden Hour", artist: "Ambient Works", duration: "5:01", color: "#f1e05a" },
{ title: "Deep Focus", artist: "Study Beats", duration: "6:28", color: "#7ee787" },
];
// Free CC0 sample
const SRC = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3";
export default function AudioPlayerRC() {
const audioRef = useRef<HTMLAudioElement>(null);
const [trackIdx, setTrackIdx] = useState(0);
const [playing, setPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(0.8);
const track = PLAYLIST[trackIdx];
const pct = duration ? (currentTime / duration) * 100 : 0;
function togglePlay() {
const a = audioRef.current;
if (!a) return;
if (a.paused) {
a.play();
setPlaying(true);
} else {
a.pause();
setPlaying(false);
}
}
function goTo(idx: number) {
setTrackIdx(idx);
setCurrentTime(0);
setPlaying(false);
}
function prev() {
goTo((trackIdx - 1 + PLAYLIST.length) % PLAYLIST.length);
}
function next() {
goTo((trackIdx + 1) % PLAYLIST.length);
}
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div className="w-full max-w-sm bg-[#161b22] border border-[#30363d] rounded-2xl overflow-hidden">
{/* Album art area */}
<div
className="h-40 flex items-center justify-center"
style={{ background: `${track.color}18` }}
>
<div
className="w-20 h-20 rounded-2xl flex items-center justify-center text-4xl shadow-lg"
style={{
background: track.color,
transform: playing ? "rotate(5deg)" : "rotate(0deg)",
transition: "transform 0.3s",
}}
>
🎵
</div>
</div>
<div className="p-5">
<div className="mb-4">
<p className="text-[#e6edf3] font-bold text-base">{track.title}</p>
<p className="text-[#8b949e] text-sm">{track.artist}</p>
</div>
{/* Progress */}
<div className="mb-4">
<audio
ref={audioRef}
src={SRC}
onTimeUpdate={() => setCurrentTime(audioRef.current?.currentTime ?? 0)}
onLoadedMetadata={() => setDuration(audioRef.current?.duration ?? 0)}
onEnded={next}
volume={volume}
/>
<input
type="range"
min={0}
max={duration || 100}
step={0.1}
value={currentTime}
onChange={(e) => {
if (audioRef.current) audioRef.current.currentTime = Number(e.target.value);
}}
className="w-full h-1 accent-[#58a6ff] cursor-pointer mb-1"
/>
<div className="flex justify-between text-[11px] text-[#484f58] tabular-nums">
<span>{formatTime(currentTime)}</span>
<span>{track.duration}</span>
</div>
</div>
{/* Controls */}
<div className="flex items-center justify-center gap-6 mb-4">
<button
onClick={prev}
className="text-[#8b949e] hover:text-[#e6edf3] transition-colors"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<polygon points="19 20 9 12 19 4 19 20" />
<line x1="5" y1="19" x2="5" y2="5" stroke="currentColor" strokeWidth="2" />
</svg>
</button>
<button
onClick={togglePlay}
className="w-12 h-12 rounded-full flex items-center justify-center transition-all"
style={{ background: track.color }}
>
{playing ? (
<svg width="18" height="18" viewBox="0 0 24 24" fill="#0d1117">
<rect x="6" y="4" width="4" height="16" />
<rect x="14" y="4" width="4" height="16" />
</svg>
) : (
<svg width="18" height="18" viewBox="0 0 24 24" fill="#0d1117">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
)}
</button>
<button
onClick={next}
className="text-[#8b949e] hover:text-[#e6edf3] transition-colors"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 4 15 12 5 20 5 4" />
<line x1="19" y1="5" x2="19" y2="19" stroke="currentColor" strokeWidth="2" />
</svg>
</button>
</div>
{/* Volume */}
<div className="flex items-center gap-2">
<span className="text-xs">🔈</span>
<input
type="range"
min={0}
max={1}
step={0.05}
value={volume}
onChange={(e) => {
setVolume(Number(e.target.value));
if (audioRef.current) audioRef.current.volume = Number(e.target.value);
}}
className="flex-1 h-1 accent-[#8b949e] cursor-pointer"
/>
<span className="text-xs">🔊</span>
</div>
</div>
{/* Playlist */}
<div className="border-t border-[#30363d] divide-y divide-[#21262d]">
{PLAYLIST.map((t, i) => (
<button
key={t.title}
onClick={() => goTo(i)}
className={`w-full flex items-center gap-3 px-5 py-3 text-left transition-colors hover:bg-white/[0.03] ${i === trackIdx ? "bg-white/[0.04]" : ""}`}
>
<div
className="w-7 h-7 rounded-lg flex items-center justify-center text-sm flex-shrink-0"
style={{ background: `${t.color}30` }}
>
{i === trackIdx && playing ? (
"▶"
) : (
<span className="text-[#484f58] text-xs">{i + 1}</span>
)}
</div>
<div className="flex-1 min-w-0">
<p
className={`text-sm truncate ${i === trackIdx ? "text-[#e6edf3] font-semibold" : "text-[#8b949e]"}`}
>
{t.title}
</p>
<p className="text-xs text-[#484f58] truncate">{t.artist}</p>
</div>
<span className="text-xs text-[#484f58] tabular-nums">{t.duration}</span>
</button>
))}
</div>
</div>
</div>
);
}<script setup>
import { ref, computed } from "vue";
const PLAYLIST = [
{ title: "Midnight Drive", artist: "Lo-fi Collective", duration: "3:42", color: "#bc8cff" },
{ title: "Rain & Coffee", artist: "Chill Vibes", duration: "4:15", color: "#58a6ff" },
{ title: "Golden Hour", artist: "Ambient Works", duration: "5:01", color: "#f1e05a" },
{ title: "Deep Focus", artist: "Study Beats", duration: "6:28", color: "#7ee787" },
];
const SRC = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3";
const audioEl = ref(null);
const trackIdx = ref(0);
const playing = ref(false);
const currentTime = ref(0);
const duration = ref(0);
const volume = ref(0.8);
const track = computed(() => PLAYLIST[trackIdx.value]);
function formatTime(s) {
if (!isFinite(s)) return "0:00";
return `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
}
function togglePlay() {
if (!audioEl.value) return;
if (audioEl.value.paused) {
audioEl.value.play();
playing.value = true;
} else {
audioEl.value.pause();
playing.value = false;
}
}
function goTo(idx) {
trackIdx.value = idx;
currentTime.value = 0;
playing.value = false;
}
function prev() {
goTo((trackIdx.value - 1 + PLAYLIST.length) % PLAYLIST.length);
}
function next() {
goTo((trackIdx.value + 1) % PLAYLIST.length);
}
function onSeek(e) {
if (audioEl.value) audioEl.value.currentTime = Number(e.target.value);
}
function onVol(e) {
volume.value = Number(e.target.value);
if (audioEl.value) audioEl.value.volume = volume.value;
}
function onTimeUpdate() {
currentTime.value = audioEl.value?.currentTime ?? 0;
}
function onLoaded() {
duration.value = audioEl.value?.duration ?? 0;
}
</script>
<template>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div class="w-full max-w-sm bg-[#161b22] border border-[#30363d] rounded-2xl overflow-hidden">
<div class="h-40 flex items-center justify-center" :style="{ background: track.color + '18' }">
<div class="w-20 h-20 rounded-2xl flex items-center justify-center text-4xl shadow-lg" :style="{ background: track.color, transform: playing ? 'rotate(5deg)' : 'rotate(0deg)', transition: 'transform 0.3s' }">🎵</div>
</div>
<div class="p-5">
<div class="mb-4">
<p class="text-[#e6edf3] font-bold text-base">{{ track.title }}</p>
<p class="text-[#8b949e] text-sm">{{ track.artist }}</p>
</div>
<div class="mb-4">
<audio ref="audioEl" :src="SRC" @timeupdate="onTimeUpdate" @loadedmetadata="onLoaded" @ended="next"></audio>
<input type="range" :min="0" :max="duration || 100" step="0.1" :value="currentTime" @input="onSeek" class="w-full h-1 accent-[#58a6ff] cursor-pointer mb-1"/>
<div class="flex justify-between text-[11px] text-[#484f58] tabular-nums">
<span>{{ formatTime(currentTime) }}</span><span>{{ track.duration }}</span>
</div>
</div>
<div class="flex items-center justify-center gap-6 mb-4">
<button @click="prev" class="text-[#8b949e] hover:text-[#e6edf3] transition-colors">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><polygon points="19 20 9 12 19 4 19 20"/><line x1="5" y1="19" x2="5" y2="5" stroke="currentColor" stroke-width="2"/></svg>
</button>
<button @click="togglePlay" class="w-12 h-12 rounded-full flex items-center justify-center transition-all" :style="{ background: track.color }">
<svg v-if="playing" width="18" height="18" viewBox="0 0 24 24" fill="#0d1117"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="#0d1117"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</button>
<button @click="next" class="text-[#8b949e] hover:text-[#e6edf3] transition-colors">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19" stroke="currentColor" stroke-width="2"/></svg>
</button>
</div>
<div class="flex items-center gap-2">
<span class="text-xs">🔈</span>
<input type="range" :min="0" :max="1" step="0.05" :value="volume" @input="onVol" class="flex-1 h-1 accent-[#8b949e] cursor-pointer"/>
<span class="text-xs">🔊</span>
</div>
</div>
<div class="border-t border-[#30363d] divide-y divide-[#21262d]">
<button v-for="(t, i) in PLAYLIST" :key="t.title" @click="goTo(i)" :class="['w-full flex items-center gap-3 px-5 py-3 text-left transition-colors hover:bg-white/[0.03]', i === trackIdx ? 'bg-white/[0.04]' : '']">
<div class="w-7 h-7 rounded-lg flex items-center justify-center text-sm flex-shrink-0" :style="{ background: t.color + '30' }">
<template v-if="i === trackIdx && playing">▶</template>
<span v-else class="text-[#484f58] text-xs">{{ i + 1 }}</span>
</div>
<div class="flex-1 min-w-0">
<p :class="['text-sm truncate', i === trackIdx ? 'text-[#e6edf3] font-semibold' : 'text-[#8b949e]']">{{ t.title }}</p>
<p class="text-xs text-[#484f58] truncate">{{ t.artist }}</p>
</div>
<span class="text-xs text-[#484f58] tabular-nums">{{ t.duration }}</span>
</button>
</div>
</div>
</div>
</template><script>
const PLAYLIST = [
{ title: "Midnight Drive", artist: "Lo-fi Collective", duration: "3:42", color: "#bc8cff" },
{ title: "Rain & Coffee", artist: "Chill Vibes", duration: "4:15", color: "#58a6ff" },
{ title: "Golden Hour", artist: "Ambient Works", duration: "5:01", color: "#f1e05a" },
{ title: "Deep Focus", artist: "Study Beats", duration: "6:28", color: "#7ee787" },
];
const SRC = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3";
let audioEl;
let trackIdx = 0;
let playing = false;
let currentTime = 0;
let duration = 0;
let volume = 0.8;
$: track = PLAYLIST[trackIdx];
$: pct = duration ? (currentTime / duration) * 100 : 0;
function formatTime(s) {
if (!isFinite(s)) return "0:00";
return `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
}
function togglePlay() {
if (!audioEl) return;
if (audioEl.paused) {
audioEl.play();
playing = true;
} else {
audioEl.pause();
playing = false;
}
}
function goTo(idx) {
trackIdx = idx;
currentTime = 0;
playing = false;
}
function prev() {
goTo((trackIdx - 1 + PLAYLIST.length) % PLAYLIST.length);
}
function next() {
goTo((trackIdx + 1) % PLAYLIST.length);
}
function onSeek(e) {
if (audioEl) audioEl.currentTime = Number(e.target.value);
}
function onVol(e) {
volume = Number(e.target.value);
if (audioEl) audioEl.volume = volume;
}
</script>
<div class="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div class="w-full max-w-sm bg-[#161b22] border border-[#30363d] rounded-2xl overflow-hidden">
<div class="h-40 flex items-center justify-center" style="background:{track.color}18">
<div class="w-20 h-20 rounded-2xl flex items-center justify-center text-4xl shadow-lg" style="background:{track.color};transform:{playing ? 'rotate(5deg)' : 'rotate(0deg)'};transition:transform 0.3s">🎵</div>
</div>
<div class="p-5">
<div class="mb-4">
<p class="text-[#e6edf3] font-bold text-base">{track.title}</p>
<p class="text-[#8b949e] text-sm">{track.artist}</p>
</div>
<div class="mb-4">
<audio bind:this={audioEl} src={SRC} on:timeupdate={() => currentTime = audioEl?.currentTime ?? 0} on:loadedmetadata={() => duration = audioEl?.duration ?? 0} on:ended={next}></audio>
<input type="range" min="0" max={duration || 100} step="0.1" value={currentTime} on:input={onSeek} class="w-full h-1 accent-[#58a6ff] cursor-pointer mb-1"/>
<div class="flex justify-between text-[11px] text-[#484f58] tabular-nums">
<span>{formatTime(currentTime)}</span><span>{track.duration}</span>
</div>
</div>
<div class="flex items-center justify-center gap-6 mb-4">
<button on:click={prev} class="text-[#8b949e] hover:text-[#e6edf3] transition-colors">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><polygon points="19 20 9 12 19 4 19 20"/><line x1="5" y1="19" x2="5" y2="5" stroke="currentColor" stroke-width="2"/></svg>
</button>
<button on:click={togglePlay} class="w-12 h-12 rounded-full flex items-center justify-center transition-all" style="background:{track.color}">
{#if playing}
<svg width="18" height="18" viewBox="0 0 24 24" fill="#0d1117"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
{:else}
<svg width="18" height="18" viewBox="0 0 24 24" fill="#0d1117"><polygon points="5 3 19 12 5 21 5 3"/></svg>
{/if}
</button>
<button on:click={next} class="text-[#8b949e] hover:text-[#e6edf3] transition-colors">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19" stroke="currentColor" stroke-width="2"/></svg>
</button>
</div>
<div class="flex items-center gap-2">
<span class="text-xs">🔈</span>
<input type="range" min="0" max="1" step="0.05" value={volume} on:input={onVol} class="flex-1 h-1 accent-[#8b949e] cursor-pointer"/>
<span class="text-xs">🔊</span>
</div>
</div>
<div class="border-t border-[#30363d] divide-y divide-[#21262d]">
{#each PLAYLIST as t, i}
<button on:click={() => goTo(i)} class="w-full flex items-center gap-3 px-5 py-3 text-left transition-colors hover:bg-white/[0.03] {i === trackIdx ? 'bg-white/[0.04]' : ''}">
<div class="w-7 h-7 rounded-lg flex items-center justify-center text-sm flex-shrink-0" style="background:{t.color}30">
{#if i === trackIdx && playing}▶{:else}<span class="text-[#484f58] text-xs">{i + 1}</span>{/if}
</div>
<div class="flex-1 min-w-0">
<p class="text-sm truncate {i === trackIdx ? 'text-[#e6edf3] font-semibold' : 'text-[#8b949e]'}">{t.title}</p>
<p class="text-xs text-[#484f58] truncate">{t.artist}</p>
</div>
<span class="text-xs text-[#484f58] tabular-nums">{t.duration}</span>
</button>
{/each}
</div>
</div>
</div>Audio Player
A premium audio playback component featuring a modern interface, playlist management, and visual feedback for current track progress.
Features
- Play, Pause, Next, Previous controls
- Playlist support with active track highlighting
- Dynamic album art and track info
- Interactive seeker bar
- Volume and mute management
- CSS animations for transitions