what we're building
a compact audio player with a round wheel of playback controls on one side (play/pause, skip forward, skip back, and volume), and a little pill-shaped panel on the other side showing the current song title, a seek bar, and the time. press play below and it actually works:
live example:
three moving parts make this work together: an <audio> element that does the actual sound playing, an array of song objects in javascript that acts as the playlist, and a handful of functions that connect the buttons to that audio element. once you see how those three talk to each other, the whole thing stops feeling like magic.
step 1 — html: the wheel
the round control wheel is a circle made of nested divs, with a small table inside it holding the three main buttons in a row:
<div class="wheel">
<div class="wheelcontrols wheelcontrols--top">
<button class="fas fa-plus" onclick="volumeUp()"></button>
</div>
<table class="wheelcontrols">
<th>
<button class="fas fa-backward" onclick="prevTrack()"></button>
</th>
<th class="innerwheel">
<button class="playpause-track fas fa-play" onclick="playpauseTrack()"></button>
</th>
<th>
<button class="fas fa-forward" onclick="nextTrack()"></button>
</th>
</table>
<div class="wheelcontrols wheelcontrols--bottom">
<button class="fas fa-minus" onclick="volumeDown()"></button>
</div>
</div>
- every single button here is empty, no text inside it at all. the icon you actually see is coming from font awesome, a free icon library.
class="fas fa-plus"tells font awesome "render a solid plus icon on this element",fa-backward,fa-forward, andfa-playdo the same for their respective icons. for this to work at all, font awesome's stylesheet has to be linked in your<head>(explained below) - the
onclick="..."attributes are the simplest possible way to wire a button to a javascript function, the moment that specific button is clicked, the function named inside the quotes runs. i'll walk through what each of those four functions actually does starting in step 4 - the outer
.wheelis just the plus button, the row of three, and the minus button, stacked vertically, css turns it into an actual circle, which i get to in step 3 - the plus and minus wrappers both carry the base
wheelcontrolsclass, plus a modifier:wheelcontrols--topnudges the plus button down a few pixels so it isn't flush against the wheel's curved edge, andwheelcontrols--bottomstrips that padding back out for the minus button. keeping those two tweaks as modifier classes instead of one-off inline styles means the sizing lives in the same stylesheet as everything else, covered in step 3b - the middle button (play/pause) has two classes on it:
playpause-trackandfas fa-play. the second class is what shows the icon, but the first one is important too, it's not decorative, it's a hook the javascript uses to find this exact button later and swap its icon between play and pause. more on that in step 4
<head>, otherwise every button will just be an invisible blank circle:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css">
step 2 — html: the info panel
next to the wheel sits the pill-shaped panel with the song title, seek bar, and the actual <audio> element:
<div id="musicplayer">
<marquee scrollamount="4" class="songtitle"></marquee>
<div class="seeking">
<div class="current-time">00:00</div>
<input type="range" min="1" max="100" value="0" class="seek_slider" onchange="seekTo()">
<div class="total-duration">0:00</div>
</div>
<audio id="music" src=""></audio>
</div>
<marquee>is a genuinely old html tag that automatically scrolls its contents sideways, it's technically deprecated, but it still works in every modern browser, and honestly, it's the easiest possible way to get a scrolling song title without writing any css animation for it. it starts out empty here because it gets filled in with the actual song name by javascript, in step 4<input type="range">is the seek bar, the little draggable line you use to jump to a different point in the song. it's set up with a range of 1 to 100, which represents a percentage of the way through the song, not an actual time in seconds, that conversion happens in javascript, step 7 covers it in detail- the two empty divs,
current-timeandtotal-duration, start as placeholder text and get overwritten with real numbers once a track loads and starts playing - the
<audio id="music" src=""></audio>tag is the single most important line on the whole page, everything else is just a custom-styled remote control for this one element. it starts with an emptysrcon purpose, because the actual song file gets loaded into it by javascript the moment the page runs, which is step 5
controls to the <audio> tag. this whole tutorial exists because that default look doesn't match anyone's theme. so instead, the <audio> tag here has no controls attribute at all, it's invisible, and every button you see is a custom html element that remote-controls it through javascript instead.step 3 — css: turning divs into a circle
none of the wheel is actually round by default, divs and tables are rectangles. the circular look comes entirely from one property applied a few times:
.wheel {
border-radius: 50em;
border: 2px solid transparent;
background: linear-gradient(white, white) padding-box,
linear-gradient(to top, white, #A2A2A2) border-box;
box-shadow: 1px 1px 10px 0px rgba(128,128,128,0.27) inset;
}
border-radius: 50em: this is the actual line making it round.emis a unit that scales with font size, using a huge value like50emis a common trick to guarantee the radius is always at least half the element's width no matter its size, which is what turns a square or rectangle into a perfect circle or pill shape, without you having to calculate the exact radius by hand- the two-layer
backgroundis what creates that subtle metallic ring around the edge. the first gradient (padding-box) fills the inside of the circle white, the second gradient (border-box, going from white to grey) only shows up in the transparent2pxborder, sinceborder-boxextends all the way to the outer edge whilepadding-boxstops before it. the border itself is set totransparentso nothing but that second gradient shows through it, giving the illusion of a soft, brushed metal rim - the inset
box-shadowis what gives the circle its slightly recessed, carved-in look, rather than looking flat,insetmakes the shadow point inward from the edges instead of casting outward from the shape onto the page
the same trick, a huge border-radius plus a soft inset shadow, is reused again on .innerwheel (the ring around the play button) and on #musicplayer (the pill-shaped panel), just at different sizes and with different gradient colors.
<input type="range"> a chunky default look that's hard to restyle directly, so it has to be reset first with appearance: none before any custom css takes effect, and the track and the little draggable thumb are styled as two completely separate pieces (::-webkit-slider-runnable-track and ::-webkit-slider-thumb), since they're treated as different elements internally, even though they look like one bar to you
step 3b — the rest of the css, piece by piece
the circle trick from step 3 covers the wheel, but there's a bunch of other css doing quiet work everywhere else on this player. here's every remaining piece:
.playerand.window/.window-body:.playeris the outermost grey pill that wraps everything, it uses the same "hugeborder-radius" trick as the wheel (100pxhere) with a subtle grey-to-white gradient background to look slightly metallic..windowjust sets a fixed width (310px) and font so the whole player doesn't reflow depending on song title length, and.window-bodycenters that content withmargin: auto.flexis a tiny one-line utility class,display: flexand nothing else, it's what puts the wheel and the info panel side by side instead of stacked on top of each other, and it's reused again inside#musicplayerjust to lay the marquee out the same way. that reuse is exactly why the vertical-centering fix below lives on a separate, more specific rule instead of being added directly to.flexitself, otherwise it would also apply inside the marquee row, where it's harmless but unnecessary.window-body > .flex { align-items: center; }: without this, a flex row centers its items on the cross axis only if you tell it to, by default items with no explicit height (like#musicplayer, whose height depends on its content) get stretched to match the tallest sibling, while an item with a fixed height (like.wheel, pinned to100pxin its own CSS rule) doesn't stretch and can end up sitting flush with the top instead.align-items: centeron the row itself is the direct, unambiguous fix, both the wheel and the info panel get vertically centered against each other no matter which one ends up taller, instead of depending on which specific side you remembered to add a fix to#musicplayeris the pill-shaped panel on the right, same double-gradient-plus-inset-shadow trick as.wheel, just with a pink-tinted gradient (#F1E3F0) instead of plain white, andfloat: rightto push it away from the wheel.innerwheelis the thin ring drawn around just the play/pause button, in the middle of the wheel. sameborder-radius: 50emtrick again, but this time the "ring" is a real visible2px solid #E2E2E2border instead of a gradient, with its own softer inset shadow so it reads as a separate, slightly recessed button sitting inside the bigger wheel- the generic
button, input[type=reset], input[type=submit]reset near the top is what makes every button on this page show only its font awesome icon and nothing else:color: transparenthides any real text,border: noneandborder-radius: 0strip the browser's default button chrome, andtext-shadow: 0 0 #222is a neat trick to draw the icon back in solid black even though the text color itself is transparent (font awesome icons are technically rendered as text using an icon font, so this is what makes them visible again) .wheelcontrolsand.wheelcontrols buttonstyle the small plus/minus volume buttons and the three-button row:font-size: 14pxcontrols the icon size, andcolor: #aaa; opacity: 0.8(bumping to full opacity1on:hover) gives them that faded, unobtrusive grey look until you actually mouse over the wheel.wheelcontrols--topandwheelcontrols--bottomare small modifiers on top of that base class, just for the padding differences between the plus and minus button wrappers from step 1.playpause-trackand.playpause-track buttonbump the middle play/pause icon up tofont-size: 20pxand a slightly darker#C1C1C1, since it's the main control and should read as more prominent than the smaller skip/volume buttons around it.songtitleis the styling on the<marquee>from step 2: padding to push it away from the edges of the pill, a muted#A3A3A3grey, andfont-size: 16pxso the scrolling title is legible but doesn't dominate the small panel.controlsand.controls buttonstyle the row of three small icons under the seek bar (a sync/repeat icon, a music note, and a shuffle icon), withtable.controlsnudging the whole row over by15pxto line up with the seek bar above it. worth noting: none of these three actually have anonclickin the html, they're purely decorative in this version, placeholders for repeat/queue/shuffle features you could wire up yourself later.seekingis the flex row holding the current time, the seek bar, and the total duration side by side,justify-content: space-evenlyspaces the three out evenly across the panel's width, and.current-time/.total-durationjust add a little breathing room (padding-right/padding-left) on either side of the slider so the numbers don't touch it- the full
input[type=range]reset is what turns the browser's default seek bar (a chunky, OS-styled slider) into the thin custom one you see:appearance: nonestrips the native look entirely, then::-webkit-slider-runnable-track/::-moz-range-trackredraw just the thin2pxtrack, and.seek_slideritself adds the actual pink-grey color (#e4d5dc), rounded ends (border-radius: 8px), and a fade-inopacitytransition..seek_slider::-webkit-slider-thumbis a separate rule again, since the draggable handle is treated as its own element internally, it gets its own tiny white circle with a thin border - everything else in the stylesheet (the
@font-facepulling in "Myriad Pro", the heading sizes, the:disabled/focus states, the@media (not(hover))block) belongs to the wider tutorials site theme this player was dropped into, not the player itself, you can safely ignore or delete all of it if you're lifting just the player for your own page
step 4 — the javascript: your playlist
now for the part that actually does something. everything starts with a plain array of songs, sitting near the top of the script:
let track_list = [
{
name: "One Shot One Kill - The Cat's Whiskers",
path: "https://files.catbox.moe/fegzmf.mp3"
},
{
name: "Shooting Arrows - The Cat's Whiskers",
path: "https://files.catbox.moe/zj81lr.mp3"
}
// ...add as many as you want, same format
];
- this is a list of objects, each one representing a single song, with exactly two pieces of information: a
name(what shows up in the scrolling title) and apath(a direct link to the actual mp3 file) - you can add as many songs as you like here, the player isn't hardcoded to any specific number, it just loops through however many objects are sitting in this array
- the
pathhas to be a direct link to an mp3 file, meaning the url itself ends in.mp3and loads the raw file when opened, not a link to a youtube video, a spotify page, or a soundcloud player, none of those work here. catbox.moe is a free, simple option for hosting your own mp3s and getting a direct link like the ones above (although its server often goes down, so i'd recommend using a diferent host for your own music player. this tutorial only uses catbox because that's how the original owner of this code shared it with the placeholder links. i'll be making a full tutorial on other available options pretty soon!)
step 5 — loading a track
with a playlist array in place, loadTrack() is the function that takes one specific song from it and actually puts it into the page:
let track_index = 0;
let curr_track = document.getElementById("music");
let track_name = document.querySelector(".songtitle");
function loadTrack(track_index) {
resetValues();
curr_track.src = track_list[track_index].path;
curr_track.load();
track_name.textContent = track_list[track_index].name;
updateTimer = setInterval(seekUpdate, 1000);
curr_track.addEventListener("ended", nextTrack);
}
loadTrack(track_index);
track_index: a plain number that keeps track of which song in the array is currently loaded, starting at0, the very first song. this one variable is the entire memory of "where we are" in the playlistdocument.getElementById("music"): grabs the actual<audio>tag from step 2 by itsid, and stores a reference to it incurr_track, so the rest of the script can talk to it without searching the page again every timecurr_track.src = track_list[track_index].path: this is the line that swaps which song is loaded.track_list[track_index]grabs the song object sitting at that position in the array, and.pathpulls its mp3 link back out, that link gets assigned straight to the audio element'ssrccurr_track.load(): tells the browser "you just got a new source, go fetch it", without this, the audio element would keep quietly playing whatever was loaded before, since just changing.srcalone doesn't force a reload on its owntrack_name.textContent = track_list[track_index].name: grabs that same song object'snameand drops it straight into the scrolling marquee from step 2, this is what makes the title update every time you skip trackssetInterval(seekUpdate, 1000): starts a repeating timer that runs theseekUpdatefunction once every 1000 milliseconds (one second). that function is what keeps the seek bar and the time display crawling forward while a song plays, it's covered fully in step 7curr_track.addEventListener("ended", nextTrack): this is what makes the playlist automatic instead of stopping dead after one song."ended"is a built-in event that the browser fires the instant a track finishes playing on its own, and this line says "when that happens, runnextTrack()", which is exactly the same function the skip-forward button uses- the very last line,
loadTrack(track_index), sitting outside of any function, is what loads song 0 the moment the page first opens, so there's already something ready to play before you've clicked anything
step 6 — play, pause, and switching tracks
these four functions are what every wheel button in step 1 is actually calling:
let isPlaying = false;
function playpauseTrack() {
if (!isPlaying) playTrack();
else pauseTrack();
}
function playTrack() {
curr_track.play();
isPlaying = true;
document.querySelector(".playpause-track").className = "playpause-track fas fa-pause";
}
function pauseTrack() {
curr_track.pause();
isPlaying = false;
document.querySelector(".playpause-track").className = "playpause-track fas fa-play";
}
function nextTrack() {
if (track_index < track_list.length - 1) track_index += 1;
else track_index = 0;
loadTrack(track_index);
if (isPlaying) playTrack();
}
function prevTrack() {
if (track_index > 0) track_index -= 1;
else track_index = track_list.length - 1;
loadTrack(track_index);
if (isPlaying) playTrack();
}
isPlaying: a simple true/false flag that keeps track of whether audio is currently playing, since<audio>elements don't just hand you that information directly, the script has to remember it on its ownplaypauseTrack(): this is the function the middle button actually calls. it doesn't do any playing or pausing itself, it just checks theisPlayingflag and decides which of the other two functions to hand off to, one button, two possible outcomesplayTrack()/pauseTrack(): each one calls the audio element's own built-in.play()or.pause()method (these come free with every<audio>tag, you don't write them yourself), flips theisPlayingflag to match, and then swaps the play/pause button's icon by directly overwriting itsclassName. this is exactly why that button needed theplaypause-trackclass back in step 1, it's the hookdocument.querySelector(".playpause-track")uses to find that specific button and change what icon it's showingnextTrack():track_index + 1moves forward one song, but theifcheck stops it from running past the end of the array, once you're on the last song,track_index < track_list.length - 1becomes false, so it wraps back around to0instead, looping the whole playlist forever instead of just stoppingprevTrack(): the mirror image ofnextTrack(), it steps backward instead, and wraps around to the last song in the array (track_list.length - 1) if you try to go back from the very first one- both skip functions end the same way:
loadTrack()to swap in the new song, then a check that callsplayTrack()again only if a song was already playing. this matters becauseloadTrack()always leaves the audio paused after loading a fresh source, without this last check, skipping tracks would silently pause your music every single time, even mid-song
step 7 — the seek bar and the clock
this is the part that keeps the seek bar crawling forward and the time counting up while a song plays, and lets you drag the bar to jump around:
function seekUpdate() {
let seekPosition = 0;
if (!isNaN(curr_track.duration)) {
seekPosition = curr_track.currentTime * (100 / curr_track.duration);
seek_slider.value = seekPosition;
let currentMinutes = Math.floor(curr_track.currentTime / 60);
let currentSeconds = Math.floor(curr_track.currentTime - currentMinutes * 60);
let durationMinutes = Math.floor(curr_track.duration / 60);
let durationSeconds = Math.floor(curr_track.duration - durationMinutes * 60);
if (currentSeconds < 10) currentSeconds = "0" + currentSeconds;
if (durationSeconds < 10) durationSeconds = "0" + durationSeconds;
curr_time.textContent = currentMinutes + ":" + currentSeconds;
total_duration.textContent = durationMinutes + ":" + durationSeconds;
}
}
function seekTo() {
let seekto = curr_track.duration * (seek_slider.value / 100);
curr_track.currentTime = seekto;
}
- remember from step 5,
seekUpdateis set to run automatically once every second bysetInterval, this function is never called manually, it just quietly keeps itself up to date in the background the whole time a track is loaded if (!isNaN(curr_track.duration)): right when a new track has just loaded, the browser hasn't figured out how long the file is yet, socurr_track.durationis temporarilyNaN("not a number"). this check just skips the whole update quietly for that split second, instead of crashing trying to do math with a value that isn't a real number yetcurr_track.currentTime * (100 / curr_track.duration): this is the conversion from an actual time in seconds into the 1 to 100 range the seek slider understands. as an example, if a song is 200 seconds long total and you're currently 50 seconds in, that's50 * (100 / 200), which works out to25, meaning the slider should sit at 25% of the way across, exactly where you'd expect for a quarter of the way through the song- the four
Math.floor(...)lines are just converting a raw seconds count into a clean minutes-and-seconds display, dividing by 60 and rounding down gets the whole minutes, and subtracting that back out gets the leftover seconds - the two
if (...Seconds < 10)checks handle the classic "8 seconds" vs "08 seconds" problem, without them, a time like 3 minutes 8 seconds would display as the slightly odd-looking3:8instead of the standard3:08 seekTo(), attached to the slider'sonchangeback in step 2, does the exact opposite conversion. it reads wherever you just dragged the slider to (a number from 1 to 100), turns that percentage back into an actual number of seconds usingcurr_track.duration * (value / 100), and then setscurr_track.currentTimeto that number, which is what actually makes the audio jump to that point in the song
volumeUp() and volumeDown() just nudge audio.volume up or down by 0.2 each click, clamped between 0.0 (silent) and 1.0 (full volume), that property is another one built directly into every audio element, you're not building volume control from scratch, just adjusting a number it already understands.the complete css
everything from step 3 and step 3b, all in one place, exactly as it appears in this page's <style> tag:
/* ============================================================
1. IPOD SHELL — outer pill, the fixed-width window, and the
flex row that lays the wheel + info panel side by side
============================================================ */
.player {
width: fit-content;
border: #CECECE solid 2px;
border-radius: 100px;
margin-left: auto;
margin-right: auto;
background: linear-gradient(0deg, rgba(205, 205, 205, 1) 0%, rgba(230, 230, 230, 1) 30%, rgba(255, 255, 255, 1) 100%);
padding: 5px;
}
.window,
.title-bar {
font-family: "Myriad Pro";
-webkit-font-smoothing: none;
font-size: 11px;
}
.window {
padding: 8px;
width: 330px;
}
.window-body {
display: block;
margin: auto;
border-radius: 0em;
}
.flex {
display: flex;
}
.window-body >
.flex {
align-items: center;
}
/* ================================================
2. WHEEL — the round control dial (plus/minus
+ the play/pause/skip row in the middle)
================================================== */
.wheel {
height: 100px;
width: 100px;
display: block;
justify-content: center;
margin: auto;
background: linear-gradient(white, white) padding-box, linear-gradient(to top, white, #A2A2A2) border-box;
border-radius: 50em;
border: 2px solid transparent;
padding-top: 0px;
box-shadow: 1px 1px 10px 0px rgba(128, 128, 128, 0.27) inset;
-webkit-box-shadow: 1px 1px 10px 0px rgba(128, 128, 128, 0.27) inset;
-moz-box-shadow: 1px 1px 10px 0px rgba(128, 128, 128, 0.27) inset;
}
.innerwheel {
border-radius: 50em;
border: 2px solid #E2E2E2;
padding: 9px;
padding-left: 10px;
padding-right: 12px;
margin: 0;
box-shadow: 1px 1px 10px 0px rgba(128, 128, 128, 0.17) inset;
-webkit-box-shadow: 1px 1px 10px 0px rgba(128, 128, 128, 0.17) inset;
-moz-box-shadow: 1px 1px 10px 0px rgba(128, 128, 128, 0.17) inset;
}
th {
width: 20px;
margin: 0;
}
.fas fa-minus {
margins: 0;
padding-bottom: 5px;
}
.wheelcontrols {
font-size: 14px !important;
text-align: center;
color: #aaa;
opacity: 0.8;
}
.wheelcontrols--top {
padding-top: 4px;
padding-bottom: 0;
}
.wheelcontrols--bottom {
padding: 0;
}
.wheelcontrols button {
background: none;
border: none;
color: #aaa;
opacity: 0.6;
font-size: 14px;
padding: 2px;
}
.wheelcontrols button:hover {
opacity: 1;
}
.playpause-track {
font-size: 22px !important;
padding: 3px;
}
.playpause-track button {
display: block;
color: #C1C1C1;
font-size: 20px;
margin: auto;
}
/* =============================================
3. INFO PANEL — song title, seek bar row,
& the decorative controls row underneath it
=============================================== */
#musicplayer {
align-items: center;
display: block;
float: right;
background: linear-gradient(0deg, #F1E3F0, white) padding-box, linear-gradient(to top, white, #A2A2A2) border-box;
border-radius: 60em;
border: 2px solid transparent;
margin-left: 8px;
padding-left: 10px;
box-shadow: 1px 1px 10px 0px rgba(128, 128, 128, 0.27) inset;
-webkit-box-shadow: 1px 1px 10px 0px rgba(128, 128, 128, 0.27) inset;
-moz-box-shadow: 1px 1px 10px 0px rgba(128, 128, 128, 0.27) inset;
}
.songtitle {
padding: 25px;
color: #A3A3A3;
padding-bottom: 0;
margin-left: 15px;
margin-right: 20px;
font-size: 16px;
display: block;
font-family: Myriad Pro;
}
.seeking {
display: flex;
justify-content: space-evenly;
align-items: center;
padding: 10px;
padding-left: 0;
padding-bottom: 0px;
color: #A3A3A3;
}
.current-time {
padding-right: 5px;
}
.total-duration {
padding-left: 5px;
}
table.controls {
margin-left: 15px;
}
.controls button {
display: block;
margin-left: 15px;
font-size: 14px !important;
text-align: center;
color: #ccb3be;
opacity: 0.6;
margin-bottom: 8px;
}
.controls button:hover {
opacity: 1;
}
/* ============================================================
4. SEEK BAR — resets the browser's native <input type=range>
look, then redraws the track and the draggable thumb
============================================================ */
.seek_slider {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
height: 6px;
background: #e4d5dc;
opacity: 0.7;
-webkit-transition: .2s;
transition: opacity .2s;
border-radius: 8px;
}
.seek_slider::-webkit-slider-thumb {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 8px;
height: 8px;
background: white;
cursor: pointer;
border-radius: 50%;
border: 1px solid #cecece;
padding-top: 3px;
position: relative;
bottom: 3px;
}
input[type=range] {
-webkit-appearance: none;
appearance: none;
width: 100%;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 2px;
}
input[type=range]::-moz-range-track {
width: 100%;
height: 2px;
}
/* ============================================================
5. ICON-ONLY BUTTON RESET — strips the browser's default
button look and hides real button text so only the font
awesome icon shows. reused site-wide, not player-specific
============================================================ */
button,
input,
label,
option,
select,
table,
textarea,
ul.tree-view {
-webkit-font-smoothing: none;
font-family: "Myriad Pro";
font-size: 11px
}
button,
input[type=reset],
input[type=submit] {
border: none;
border-radius: 0;
box-sizing: border-box;
color: transparent;
min-height: 23px;
min-width: 75px;
padding: 0 12px;
text-shadow: 0 0 #222
}
button {
min-width: 20px;
background: none;
text-align: center;
}
.vertical-bar,
button,
input[type=reset],
input[type=submit].vertical-bar {
height: 20px;
width: 4px
}
button:not(:disabled):active,
input[type=reset]:not(:disabled):active,
input[type=submit]:not(:disabled):active {
opacity: 0.5
}
button:active {
opacity: 0.7;
}
@media (not(hover)) {
button:not(:disabled):hover,
input[type=reset]:not(:disabled):hover,
input[type=submit]:not(:disabled):hover {
box-shadow: inset -1px -1px #fff, inset 1px 1px #0a0a0a, inset -2px -2px #dfdfdf, inset 2px 2px grey
}
}
button:focus,
input[type=reset]:focus,
input[type=submit]:focus {
opacity: 1
}
button::-moz-focus-inner,
input[type=reset]::-moz-focus-inner,
input[type=submit]::-moz-focus-inner {
border: 0
}
:disabled,
:disabled+label,
input[readonly],
input[readonly]+label {
color: grey
}
:disabled+label,
button:disabled,
input[type=reset]:disabled,
input[type=submit]:disabled {
text-shadow: 1px 1px 0 #fff
}
/* ============================================================
6. SITE TYPOGRAPHY — the wider tutorials theme, unrelated
to the player, safe to delete if you're only lifting the
player for your own page
============================================================ */
@font-face {
font-family: "Myriad Pro";
src: url("https://dl.dropbox.com/scl/fi/z8hqw29h8a9i3a3cbbxkt/MYRIADPRO-REGULAR.OTF?rlkey=begwqxljs2gzyw26h00oeovzi&st=px0mbeax&dl=0") format("woff");
}
h1 {
font-size: 5rem
}
h2 {
font-size: 2.5rem
}
h3 {
font-size: 2rem
}
h4 {
font-size: 1.5rem
}
u {
border-bottom: .5px solid #222;
text-decoration: none
}
the complete javascript
and here's the full script from steps 4 through 7, unedited and in the order it actually runs in on the page:
<script>
(function() {
let track_name, playpause_btn, seek_slider, curr_time, total_duration, curr_track;
let track_index = 0;
let isPlaying = false;
let updateTimer;
let volume = 0.8;
let track_list = [{
name: "One Shot One Kill - The Cat's Whiskers",
path: "https://files.catbox.moe/fegzmf.mp3"
},
{
name: "Shooting Arrows - The Cat's Whiskers",
path: "https://files.catbox.moe/zj81lr.mp3"
},
{
name: "4 REAL - The Cat's Whiskers",
path: "https://files.catbox.moe/fxd8fo.mp3"
},
{
name: "My Sweetest Love - The Cat's Whiskers ft. Kazuma Mitchell",
path: "https://files.catbox.moe/qe4he5.mp3"
},
{
name: "Mercy On Me - The Cat's Whiskers",
path: "https://files.catbox.moe/w7nnf9.mp3"
}
];
function resetValues() {
curr_time.textContent = "00:00";
total_duration.textContent = "0:00";
seek_slider.value = 0;
}
function loadTrack(i) {
clearInterval(updateTimer);
resetValues();
curr_track.src = track_list[i].path;
curr_track.load();
curr_track.volume = volume;
track_name.textContent = track_list[i].name;
updateTimer = setInterval(seekUpdate, 1000);
curr_track.addEventListener("ended", nextTrack);
}
function playTrack() {
curr_track.play();
isPlaying = true;
playpause_btn.className = "playpause-track fas fa-pause";
}
function pauseTrack() {
curr_track.pause();
isPlaying = false;
playpause_btn.className = "playpause-track fas fa-play";
}
window.playpauseTrack = function() {
if (!isPlaying) playTrack();
else pauseTrack();
};
window.nextTrack = nextTrack;
function nextTrack() {
if (track_index < track_list.length - 1) track_index += 1;
else track_index = 0;
loadTrack(track_index);
if (isPlaying) playTrack();
}
window.prevTrack = function() {
if (track_index > 0) track_index -= 1;
else track_index = track_list.length - 1;
loadTrack(track_index);
if (isPlaying) playTrack();
};
window.volumeUp = function() {
if (volume < 1.0) {
volume = Math.min(1, volume + 0.2);
curr_track.volume = volume;
}
};
window.volumeDown = function() {
if (volume > 0.0) {
volume = Math.max(0, volume - 0.2);
curr_track.volume = volume;
}
};
window.seekTo = function() {
let seekto = curr_track.duration * (seek_slider.value / 100);
curr_track.currentTime = seekto;
};
function seekUpdate() {
let seekPosition = 0;
if (!isNaN(curr_track.duration)) {
seekPosition = curr_track.currentTime * (100 / curr_track.duration);
seek_slider.value = seekPosition;
let currentMinutes = Math.floor(curr_track.currentTime / 60);
let currentSeconds = Math.floor(curr_track.currentTime - currentMinutes * 60);
let durationMinutes = Math.floor(curr_track.duration / 60);
let durationSeconds = Math.floor(curr_track.duration - durationMinutes * 60);
if (currentSeconds < 10) currentSeconds = "0" + currentSeconds;
if (durationSeconds < 10) durationSeconds = "0" + durationSeconds;
curr_time.textContent = currentMinutes + ":" + currentSeconds;
total_duration.textContent = durationMinutes + ":" + durationSeconds;
}
}
document.addEventListener("DOMContentLoaded", function() {
track_name = document.querySelector(".songtitle");
playpause_btn = document.querySelector(".playpause-track");
seek_slider = document.querySelector(".seek_slider");
curr_time = document.querySelector(".current-time");
total_duration = document.querySelector(".total-duration");
curr_track = document.getElementById("music");
loadTrack(track_index);
});
})();
</script>
result
a fully working playlist player: one array of songs holding all your music, one <audio> element doing the real playback work behind the scenes, and a set of small functions translating button clicks into calls on that element's built-in .play(), .pause(), .load(), .currentTime, and .volume. none of it is actually complicated once it's broken apart like this, the trickiest bit is just remembering to keep track_index, the isPlaying flag, and the audio element's own state all in sync with each other. swap the ids and class names for your own theme's styling and drop in your own track_list, and it's yours. again, full credit to @b8nadryl for the original design! :3
note: if you want to explore more music players options and use a different style, i highly recommend checking out this page ♡