Music Player App with HTML, CSS & JavaScript
20 DAYS 20 PROJECT CHALLENGE
Day #20
Project Overview
The Music Player App is a simple and interactive mini player built using HTML, CSS, and JavaScript. It uses the HTML5 Audio API to play music, update the progress bar in real time, and manage playback states. The app displays the current songโs cover, title, artist, and time, and allows users to control playback easily through buttons, volume control, and a playlist. It is fully responsive, smooth, and easy to customize with your own audio files.
Key Features
- Play & Pause the music
- Next and Previous track controls
- Clickable & draggable progress bar
- Real-time time display (current time & duration)
- Auto-play next track when one ends
- Playlist with active track highlight
- Volume control slider
- Repeat mode for looping a song
- Responsive UI with album cover & metadata
- Keyboard shortcuts (Space for play/pause, Arrows for next/prev)
HTML Code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Day 20 — Music Player App</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<main class="card" role="main" aria-labelledby="title">
<header class="top">
<div>
<h1 id="title">Day 20: Music Player App</h1>
<p class="lead">Mini music player demonstrating the Audio API, event listeners and state management.</p>
</div>
</header>
<section class="player">
<div class="now-playing">
<img id="cover" class="cover" src="https://picsum.photos/300?random=10" alt="Album art" />
<div class="meta">
<div id="trackTitle" class="title">Track Title</div>
<div id="trackArtist" class="artist">Artist</div>
<div class="progress-wrap" aria-label="Track progress">
<div id="progressContainer" class="progress-container" role="slider" tabindex="0" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div id="progress" class="progress"></div>
</div>
<div class="time-row">
<span id="currentTime">0:00</span>
<span id="duration">0:00</span>
</div>
</div>
<div class="controls">
<button id="prevBtn" class="btn" title="Previous (Arrow Left)">โฎ</button>
<button id="playPauseBtn" class="btn primary" title="Play / Pause (Space)">โถ</button>
<button id="nextBtn" class="btn" title="Next (Arrow Right)">โญ</button>
<button id="shuffleBtn" class="btn" title="Shuffle">๐</button>
<button id="repeatBtn" class="btn" title="Repeat">๐</button>
<div class="vol">
<button id="muteBtn" class="btn" title="Mute">๐</button>
<input id="volume" type="range" min="0" max="1" step="0.01" value="0.8" aria-label="Volume" />
</div>
</div>
</div>
</div>
<aside class="playlist" aria-label="Playlist">
<h3>Playlist</h3>
<ol id="playlistList" class="playlist-list"></ol>
</aside>
</section>
<audio id="audio" preload="metadata"></audio>
<details style="margin-top:12px">
<summary>How it works (short)</summary>
<p class="small">JS controls a single <audio> element. Controls call play/pause/seek functions. The `timeupdate` event updates the progress bar and time labels. Playlist is an array of track objects (src, title, artist, cover).</p>
</details>
</main>
<script src="script.js"></script>
</body>
</html>
CSS Code
:root {
--bg: #071026;
--card: #0b1220;
--accent: #06b6d4;
--accent2: #7c3aed;
--muted: #9aa4b2;
--white: #e6eef6;
font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial;
}
* {
box-sizing: border-box
}
html,
body {
height: 100%;
margin: 0;
background: #002252;
color: var(--white)
}
body {
display: flex;
align-items: center;
justify-content: center;
padding: 28px
}
.card {
width: min(980px, 96%);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
border-radius: 12px;
padding: 18px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.03);
}
.top {
display: flex;
gap: 12px;
align-items: center
}
.logo {
width: 60px;
height: 60px;
border-radius: 10px;
object-fit: cover
}
h1 {
margin: 0;
font-size: 18px
}
.lead {
margin: 4px 0 12px;
color: var(--muted);
font-size: 14px
}
.player {
display: flex;
gap: 18px;
align-items: flex-start;
margin-top: 12px
}
.now-playing {
display: flex;
gap: 14px;
align-items: flex-start;
flex: 1
}
.cover {
width: 160px;
height: 160px;
border-radius: 10px;
object-fit: cover;
border: 1px solid rgba(255, 255, 255, 0.04)
}
.meta {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px
}
.title {
font-size: 18px;
font-weight: 700
}
.artist {
color: var(--muted);
font-size: 14px
}
.progress-wrap {
display: flex;
flex-direction: column;
gap: 8px
}
.progress-container {
height: 10px;
background: rgba(255, 255, 255, 0.04);
border-radius: 999px;
position: relative;
cursor: pointer
}
.progress {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--accent2), var(--accent));
border-radius: 999px;
transition: width 120ms linear
}
.time-row {
display: flex;
justify-content: space-between;
color: var(--muted);
font-family: ui-monospace, monospace;
font-size: 13px
}
.controls {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap
}
.btn {
padding: 8px 10px;
border-radius: 8px;
border: 0;
background: transparent;
color: var(--white);
cursor: pointer;
font-size: 18px
}
.btn.primary {
background: linear-gradient(90deg, var(--accent2), var(--accent));
padding: 10px 14px;
border-radius: 10px
}
.vol {
display: flex;
gap: 8px;
align-items: center
}
input[type="range"] {
width: 110px
}
.playlist {
width: 300px;
border-left: 1px solid rgba(255, 255, 255, 0.03);
padding-left: 14px
}
.playlist h3 {
margin: 0 0 8px 0
}
.playlist-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px
}
.playlist-list li {
padding: 8px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.02);
cursor: pointer;
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
border: 1px solid rgba(255, 255, 255, 0.03)
}
.playlist-list li.active {
outline: 3px solid rgba(124, 58, 237, 0.08);
transform: translateY(-2px)
}
.playlist-list li .meta {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 8px
}
.playlist-list li .meta .name {
font-weight: 600
}
.playlist-list li .meta .artist {
font-size: 12px;
color: var(--muted)
}
.small {
font-size: 13px;
color: var(--muted)
}
@media (max-width:880px) {
.player {
flex-direction: column
}
.playlist {
width: 100%;
border-left: 0;
padding-left: 0
}
.cover {
width: 120px;
height: 120px
}
} Javascript Code
// Day 20 — Music Player App
// Concepts: Audio element control, events, progress seek, playlist, shuffle/repeat
// Playlist — free royalty-free MP3 URLs (Bensound) + sample covers from picsum
const TRACKS = [
{
src: "https://www.bensound.com/bensound-music/bensound-acousticbreeze.mp3",
title: "Acoustic Breeze",
artist: "Bensound",
cover: "https://picsum.photos/seed/acoustic/400/400"
},
{
src: "https://www.bensound.com/bensound-music/bensound-creativeminds.mp3",
title: "Creative Minds",
artist: "Bensound",
cover: "https://picsum.photos/seed/creative/400/400"
},
{
src: "https://www.bensound.com/bensound-music/bensound-betterdays.mp3",
title: "Better Days",
artist: "Bensound",
cover: "https://picsum.photos/seed/better/400/400"
},
{
src: "https://www.bensound.com/bensound-music/bensound-goinghigher.mp3",
title: "Going Higher",
artist: "Bensound",
cover: "https://picsum.photos/seed/higher/400/400"
},
{
src: "https://www.bensound.com/bensound-music/bensound-sunny.mp3",
title: "Sunny",
artist: "Bensound",
cover: "https://picsum.photos/seed/sunny/400/400"
}
];
// DOM refs
const audio = document.getElementById('audio');
const coverEl = document.getElementById('cover');
const trackTitleEl = document.getElementById('trackTitle');
const trackArtistEl = document.getElementById('trackArtist');
const playPauseBtn = document.getElementById('playPauseBtn');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const progressContainer = document.getElementById('progressContainer');
const progressEl = document.getElementById('progress');
const currentTimeEl = document.getElementById('currentTime');
const durationEl = document.getElementById('duration');
const volumeInput = document.getElementById('volume');
const muteBtn = document.getElementById('muteBtn');
const playlistList = document.getElementById('playlistList');
const shuffleBtn = document.getElementById('shuffleBtn');
const repeatBtn = document.getElementById('repeatBtn');
// Player state
let currentIndex = 0;
let isPlaying = false;
let isShuffle = false;
let repeatMode = 'off'; // off | one | all
let lastVolume = Number(volumeInput.value) || 0.8;
// Initialize playlist UI and audio
function init() {
buildPlaylistUI();
loadTrack(currentIndex);
attachEvents();
audio.volume = Number(volumeInput.value);
updatePlayButton();
}
// Build playlist list items
function buildPlaylistUI() {
playlistList.innerHTML = '';
TRACKS.forEach((t, i) => {
const li = document.createElement('li');
li.dataset.index = i;
li.innerHTML = `<div class="meta"><strong class="name">${t.title}</strong><div class="artist">${t.artist}</div></div>
<div class="time muted">—:—</div>`;
if (i === currentIndex) li.classList.add('active');
li.addEventListener('click', () => {
loadTrack(i);
play();
});
playlistList.appendChild(li);
});
}
// Load a track by index (does not auto-play)
function loadTrack(index) {
if (index < 0 || index >= TRACKS.length) return;
currentIndex = index;
const track = TRACKS[index];
audio.src = track.src;
coverEl.src = track.cover || 'https://picsum.photos/400';
trackTitleEl.textContent = track.title;
trackArtistEl.textContent = track.artist;
Array.from(playlistList.children).forEach(li => li.classList.toggle('active', Number(li.dataset.index) === index));
progressEl.style.width = '0%';
currentTimeEl.textContent = '0:00';
durationEl.textContent = '0:00';
audio.load();
}
// Play / Pause helpers
function play() {
audio.play().then(() => {
isPlaying = true;
updatePlayButton();
}).catch(err => {
console.error('Playback failed:', err);
});
}
function pause() {
audio.pause();
isPlaying = false;
updatePlayButton();
}
function togglePlay() {
if (isPlaying) pause(); else play();
}
function updatePlayButton() {
playPauseBtn.textContent = isPlaying ? 'โธ' : 'โถ';
}
// Next / Prev
function nextTrack() {
if (isShuffle) {
let next;
if (TRACKS.length === 1) next = 0;
else {
do { next = Math.floor(Math.random() * TRACKS.length); } while (next === currentIndex && TRACKS.length > 1);
}
loadTrack(next);
play();
return;
}
if (currentIndex < TRACKS.length - 1) {
loadTrack(currentIndex + 1);
play();
} else {
if (repeatMode === 'all') {
loadTrack(0);
play();
} else {
pause();
audio.currentTime = 0;
}
}
}
function prevTrack() {
if (audio.currentTime > 5) {
audio.currentTime = 0;
return;
}
if (currentIndex > 0) {
loadTrack(currentIndex - 1);
play();
} else {
audio.currentTime = 0;
}
}
// Format seconds to mm:ss
function fmtTime(sec) {
if (!isFinite(sec) || sec === 0) return '0:00';
const s = Math.floor(sec % 60);
const m = Math.floor(sec / 60);
return `${m}:${String(s).padStart(2, '0')}`;
}
// Attach events
function attachEvents() {
audio.addEventListener('loadedmetadata', () => {
durationEl.textContent = fmtTime(audio.duration);
const li = playlistList.querySelector(`li[data-index="${currentIndex}"]`);
if (li) {
const t = li.querySelector('.time');
if (t) t.textContent = fmtTime(audio.duration);
}
});
audio.addEventListener('timeupdate', () => {
const pct = (audio.currentTime / audio.duration) * 100 || 0;
progressEl.style.width = `${pct}%`;
currentTimeEl.textContent = fmtTime(audio.currentTime);
});
audio.addEventListener('ended', () => {
if (repeatMode === 'one') {
audio.currentTime = 0;
play();
} else {
nextTrack();
}
});
playPauseBtn.addEventListener('click', togglePlay);
prevBtn.addEventListener('click', prevTrack);
nextBtn.addEventListener('click', nextTrack);
// progress seek
let isSeeking = false;
function seekFromEvent(e) {
const rect = progressContainer.getBoundingClientRect();
const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
const pct = Math.max(0, Math.min(1, x / rect.width));
audio.currentTime = pct * (audio.duration || 0);
}
progressContainer.addEventListener('click', (e) => seekFromEvent(e));
progressContainer.addEventListener('pointerdown', (e) => {
isSeeking = true;
progressContainer.setPointerCapture(e.pointerId);
seekFromEvent(e);
});
progressContainer.addEventListener('pointermove', (e) => {
if (!isSeeking) return;
seekFromEvent(e);
});
progressContainer.addEventListener('pointerup', (e) => {
isSeeking = false;
try { progressContainer.releasePointerCapture(e.pointerId); } catch(_) {}
});
progressContainer.addEventListener('pointercancel', () => isSeeking = false);
// volume & mute
volumeInput.addEventListener('input', () => {
audio.volume = Number(volumeInput.value);
if (audio.volume > 0) {
muteBtn.textContent = '๐';
lastVolume = audio.volume;
} else {
muteBtn.textContent = '๐';
}
});
muteBtn.addEventListener('click', () => {
if (audio.volume > 0) {
lastVolume = audio.volume;
audio.volume = 0;
volumeInput.value = 0;
muteBtn.textContent = '๐';
} else {
audio.volume = lastVolume || 0.8;
volumeInput.value = audio.volume;
muteBtn.textContent = '๐';
}
});
// shuffle & repeat
shuffleBtn.addEventListener('click', () => {
isShuffle = !isShuffle;
shuffleBtn.style.opacity = isShuffle ? '1' : '0.6';
});
repeatBtn.addEventListener('click', () => {
if (repeatMode === 'off') repeatMode = 'one';
else if (repeatMode === 'one') repeatMode = 'all';
else repeatMode = 'off';
repeatBtn.textContent = repeatMode === 'off' ? '๐' : (repeatMode === 'one' ? '๐' : '๐');
repeatBtn.style.opacity = repeatMode === 'off' ? '0.7' : '1';
});
// keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
togglePlay();
} else if (e.code === 'ArrowRight') {
nextTrack();
} else if (e.code === 'ArrowLeft') {
prevTrack();
}
});
audio.addEventListener('error', (e) => {
console.error('Audio error', e);
nextTrack();
});
}
// initialize
init();
Related Projects
Day 18 : Text-to-Speech Converter
Converts entered text into speech.
Concepts: Web Speech API.
Day 14 : Image Gallery with Modal
Displays images in a grid with a pop-up modal view.
Concepts: DOM traversal, event bubbling.
Day 15 : Light/Dark Mode Toggle
Switch between light and dark themes with one click (and remember the choice).
Concepts: CSS variables, LocalStorage.