1. Web Design
  2. JavaScript

Build a Custom HTML Music Player, Using JavaScript and the Web Audio API

Scroll to top
Read Time: 16 min

This tutorial will show you how to build a custom music player, using the Web audio API, that is uniquely branded with CSS, HTML, and JavaScript. 

HTML5 and the Web Audio API are tools that allow you to own a given website’s audio playback experience.

Rather than always using the browser defaults or third-party solutions to play audio, we can tap into the supplied free APIs and deliver a more branded design to end users on the web.

With that in mind, let’s leverage JavaScript to use the Web Audio API and ultimately give the end user more control over their audio listening experience.

Our HTML Audio Player Demo

Click on the play button, listen to it go.

The final audio player features volume, play, pause, and scrubbing controls. We’ll also make it look sleek with some rich color and design, loosely based on some of the designs in this After Effects Music Player Pack on Envato Elements.

For the purposes of this tutorial our audio player is simple. It can, however, be redesigned and extended to use a track list should you need more than one audio file at a given time. 

Playback in CodePen and Google Chrome

Whilst building this audio player, I noticed an issue with the CodePen demo in Google Chrome. If you play the audio track, then refresh the page, you’ll probably find the track won’t play again. It seems the combination of CodePen’s iframes, plus Google’s Autoplay policy, conspire to give us a low media engagement score and render the track unplayable.

As you’ll see, we add the recommended code to allow for playing audio based on user interaction (a click) but we’re still getting errors in this case. Rest assured that once outside the CodePen environment our audio player works just fine in Google Chrome.

1. The HTML

Let’s begin by adding the necessary HTML to make this concept a reality! We need to have an <audio> tag element on the page to tap into the Web Audio API most efficiently. This doesn’t need to display visually by default, but it’s crucial that we have it on the page.

1
<div class="player">
2
  <div class="player-track-meta">
3
    <p>Track name</p>
4
    <p><span>Track Author</span></p>
5
  </div>
6
  <div class="player-controls">
7
    <button class="player-play-btn" 
8
            role="button" 
9
            aria-label="Play"
10
            data-playing="false"
11
            >
12
      <div class="player-icon-play">
13
     <svg xmlns="https://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>play</title><polygon class="icon-play" points="19.05 12 6 3.36 6 20.64 19.05 12"/><rect class="icon-container" width="24" height="24"/></svg>
14
        </div>
15
      
16
      <div class="player-icon-pause hidden">
17
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>pause</title><g><rect  class="icon-pause" x="6" y="3.26" width="4" height="17.48"/><rect class="icon-pause" x="14" y="3.26" width="4" height="17.48"/></g><rect class="icon-container" width="24" height="24"/></svg>
18
      </div>
19
    </button>
20
    <div class="player-timeline">
21
      <span class="player-time player-time-current">00:00</span>
22
      <div class="player-progress">
23
        <div class="player-progress-filled"></div>
24
      </div>
25
      <span class="player-time player-time-duration">00:00</span>
26
    </div>
27
    <div class="player-volume-container">
28
          <input type="range" id="volume" min="0" max="1" value="1" step="0.01" class="player-volume" />
29
    </div>
30
  </div>
31
  
32
  <audio src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/858/outfoxing.mp3" crossorigin="anonymous" ></audio>
33
</div>

Linked inside the src attribute of the <audio> tag on the page, you’ll find an MP3 audio track.

info
I used “Outfoxing the Fox” by Kevin MacLeod, released this under Creative Commons. I borrowed it from the MDN example Boombox project.

The core player HTML markup comprises a variety of containers, controls, and SVG icons. We leverage a range input to adjust volume and swap between a play and pause icon for better use of space.

2. Styling the Player

Your music player can be as unique as you like. I chose my own preferred style, which is quite simplistic and will allow you to add and remove your own details without too much trouble. Here’s the final CSS I ended up with.

1
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
2
3
body {
4
  background: url("https://assets.codepen.io/210284/music-bg_1.jpg") center center;
5
  background-size: cover;
6
  color: #1f2937;
7
  font-family: 'Inter', sans-serif;
8
}
9
10
.hidden {
11
  display: none;
12
}
13
14
.player {
15
  max-width: 500px;
16
  margin: 7rem auto;
17
  background: white;
18
  padding: 36px 32px 24px 32px;
19
  border-radius: 14px;
20
  box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
21
}
22
23
.player-track-meta {
24
  text-align: center;
25
}
26
27
.player-track-meta p {
28
  margin: 0;
29
  font-size: 20px;
30
  color: #0e0e0e;
31
  font-weight: 700;
32
}
33
34
.player-track-meta span {
35
  font-size: 16px;
36
  font-weight: 400;
37
  padding: 0 2px;
38
  position: relative;
39
  top: 1px;
40
  color: #a3a3a3;
41
}
42
43
.player-controls {
44
  display: flex;
45
  align-items: center;
46
}
47
48
.player-play-btn {
49
  background: transparent;
50
  border: none;
51
  cursor: pointer;
52
  display: flex;
53
  justify-content: center;
54
  align-items: center;
55
  width: 36px;
56
  height: 36px;
57
}
58
59
.icon-container {
60
  fill: transparent;
61
  stroke: none;
62
}
63
64
.player-play-btn:hover {
65
  fill: #444444;
66
}
67
68
.player-play-btn svg { 
69
  color: #0e0e0e;
70
  position: relative;
71
  left: 0.5px;
72
  width: 36px;
73
  height: 36px;
74
  display: block;
75
}
76
77
.player-play-btn:hover svg {
78
  color: #ffffff;
79
}
80
81
.player-timeline {
82
  display: flex;
83
  flex: 1;
84
  align-items: center;
85
  justify-content: space-between;
86
  padding-left: 10px;
87
}
88
89
.player-progress {
90
  display: flex;
91
  postion: relative;
92
  height: 6px;
93
  background: #a3a3a3;
94
  border-radius: 25px;
95
  margin: 0 5px;
96
  flex: 10;
97
  flex-basis: 100%;
98
  overflow: hidden;
99
}
100
101
.player-progress-filled {
102
  height: 6px;
103
  background: #0e0e0e;
104
  flex: 0;
105
  flex-basis: 0%;
106
  border-radius: 25px;
107
}
108
109
.player-time {
110
  padding: 2px 5px;
111
}
112
113
.player-volume-container {
114
  width: 15%;
115
}
116
.player-volume {
117
  height: 28px;
118
  -webkit-appearance: none;
119
  margin: 10px 0;
120
  width: 100%;
121
  background: transparent;
122
}
123
124
.player-volume:focus {
125
  outline: none;
126
}
127
128
.player-volume::-webkit-slider-runnable-track {
129
  width: 100%;
130
  height: 6px;
131
  cursor: pointer;
132
  animate: 0.2s;
133
  background: #0e0e0e;
134
  border-radius: 10px;
135
}
136
137
.player-volume::-webkit-slider-thumb {
138
  height: 16px;
139
  width: 16px;
140
  border-radius: 100px;
141
  border: none;
142
  background: #0e0e0e;
143
  cursor: pointer;
144
  -webkit-appearance: none;
145
  margin-top: -4px;
146
}
147
148
.player-volume:focus::-webkit-slider-runnable-track {
149
  background: #0e0e0e;
150
}
151
152
.player-volume::-moz-range-track {
153
  width: 100%;
154
  height: 6px;
155
  cursor: pointer;
156
  animate: 0.2s;
157
  background: #0e0e0e;
158
  border-radius: 10px;
159
}
160
161
.player-volume::-moz-range-thumb {
162
  height: 16px;
163
  width: 16px;
164
  border-radius: 100px;
165
  border: none;
166
  background: #0e0e0e;
167
  cursor: pointer;
168
  margin-top: -4px;
169
}
170
171
.player-volume::-ms-track {
172
  width: 100%;
173
  height: 6px;
174
  cursor: pointer;
175
  animate: 0.2s;
176
  background: #0e0e0e;
177
  border-radius: 10px;
178
}
179
180
.player-volume::-ms-fill-lower {
181
  background: #0e0e0e;
182
  border-radius: 10px;
183
}
184
185
.player-volume::-ms-fill-upper {
186
  background: #0e0e0e;
187
  border-radius: 10px;
188
}
189
190
.player-volume::-ms-thumb {
191
  margin-top: 1px;
192
  height: 15px;
193
  width: 15px;
194
  border-radius: 5px;
195
  border: none;
196
  background: #0e0e0e;
197
  cursor: pointer;
198
}
199
200
.player-volume:focus::-ms-fill-lower {
201
  background: #38bdf8;
202
}
203
204
.player-volume:focus::-ms-fill-upper {
205
  background: #38bdf8;
206
}

In most cases, I maintained a naming convention for class names with the prefix .player- to keep things modular. A utility class of .hidden was added, which will come in handy when we tackle the JavaScript portion of this tutorial.

3. Making it All Work With JavaScript

To tap into the Web Audio API, we need some form of media element in the HTML. You can optionally create one with JavaScript, but I find it a little easier to render something on the page that’s not in view.

You’ll recall we added the <audio> tag in a previous step. We’ll query for it first and also create something known as a new AudioContext() instance.

1
// load sound via <audio> tag

2
const audioElement = document.querySelector("audio")
3
const audioCtx = new AudioContext()
4
const track = audioCtx.createMediaElementSource(audioElement)

What’s AudioContext?

AudioContext is an interface that represents an audio-processing graph built from audio modules that are linked together (AudioNodes).

That’s a complicated way of saying that an AudioContext is a grouping of audio nodes you can interact with and manipulate. Things like volume (gain), panning, and more are options available to tweak. We'll interact more with this feature coming up when we address volume controls.

The track variable allows us to adjust the audio element's properties directly using the AudioContext. Read more about the createMediaElementSource() method.

“An AudioContext is a grouping of audio nodes you can interact with and manipulate.”

Querying for Player Controls

What’s a custom player without custom controls? The next step is to query for all the custom controls we added in the HTML. We’ll using the Document.querySelector() method to return each associated element assigned by a variable.

1
// Player controls and attributes

2
const playButton = document.querySelector(".player-play-btn")
3
const playIcon = playButton.querySelector(".player-icon-play")
4
const pauseIcon = playButton.querySelector(".player-icon-pause")
5
const progress = document.querySelector(".player-progress")
6
const progressFilled = document.querySelector(".player-progress-filled")
7
const playerCurrentTime = document.querySelector(".player-time-current")
8
const playerDuration = document.querySelector(".player-time-duration")
9
const volumeControl = document.querySelector(".player-volume")

Here we have variables for each independent control and the progress bar shown in the user interface.

Waiting Before JavaScript Fires

To properly load the audio, which can sometimes take longer than other items on the page, it probably makes sense to wait for the entire page to load before we run any JavaScript.

We’ll start with an event listener that waits for the page to load. We can wrap the entirety of our code in this block.

1
window.addEventListener("load", () => {
2
  // all code goes here besides variables

3
})

We’ll start by listening for the playButton variable’s click event to instruct our player to Play.

1
// Play button toggle

2
playButton.addEventListener("click", () => {
3
  // check if context is in suspended state (autoplay policy)

4
  // By default, browsers won't allow you to autoplay audio.

5
  // You can override by finding the AudioContext state and resuming it after a user interaction like a "click" event.

6
  if (audioCtx.state === "suspended") {
7
    audioCtx.resume()
8
  }
9
10
  // Play or pause track depending on state

11
  if (playButton.dataset.playing === "false") {
12
    audioElement.play()
13
14
    playButton.dataset.playing = "true"
15
    playIcon.classList.add("hidden")
16
    pauseIcon.classList.remove("hidden")
17
  } else if (playButton.dataset.playing === "true") {
18
    audioElement.pause()
19
    playButton.dataset.playing = "false"
20
    pauseIcon.classList.add("hidden")
21
    playIcon.classList.remove("hidden")
22
  }
23
})

A few things happen at once when the playButton gets clicked.

  1. Browsers are smart enough to stop auto-playing audio from playing on the first load. Inside the AudioContext method, there is a state method that returns a value of “suspended”, “running”, or “closed”. In our case, we’ll be looking for “suspended”. If that‘s the state that returns, we can proceed to resume the audio with the method called resume().
  2. We use data attributes in the HTML to denote when the button is “playing” or “paused”.

  3. If the play or pause button is clicked, we can dynamically tell the audioElement to play or pause.

  4. For a better user experience, I added the ability to show and hide the play or pause icons depending on the player’s state.

Update Time Stamps and Progress

Each track you load with an AudioElement context will have its characteristics and metadata you can display in the HTML. We start by making everything zero on the first-page load and proceed to call a function that dynamically updates and formats the time as the audio gets played or paused.

We’ll additionally show a progress bar that will dynamically fill based on the amount of lapsed audio. This is handy for the end user who might want to glance at a progress bar rather than read the remaining time.

1
// Update progress bar and time values as audio plays

2
audioElement.addEventListener("timeupdate", () => {
3
  progressUpdate()
4
  setTimes()
5
})

I created two functions that are extracted elsewhere in the JavaScript file. The main thing to denote about the code above is the type of event listener we keep track of. The timeupdate event is unique to media like Audio or Video within the Web API.

Displaying and Formatting Time 

We can use the playerCurrentTime and playerDuration variables to display and format time. We’ll set the textContent of those tags in the HTML to match a new timestamp relative to the audioElement’s current attributes. An audioElement will have a currentTime property and a duration property.

Using the Date API in JavaScript, we can tap into a handy one-liner to convert the default seconds that get returned from currentTime and duration in a format that matches HH:MM:SS (Hours, Minutes, Seconds).

1
// Display currentTime and duration properties in real-time

2
function setTimes() {
3
  playerCurrentTime.textContent = new Date(audioElement.currentTime * 1000)
4
    .toISOString()
5
    .substr(11, 8)
6
  playerDuration.textContent = new Date(audioElement.duration * 1000)
7
    .toISOString()
8
    .substr(11, 8)
9
}

Updating Player Progress

Updating the progress bar in our HTML is relatively simple and comes down to a percentage calculation. We’ll get the percent returned by dividing the audioElement.currentTime by the audioElement.duration and multiplying that by 100.

Finally, we can set some CSS via JavaScript by using the progressFilled variable we created before and adjusting the flex-basis property to grow or shrink depending on the change percentage.

1
// Update player timeline progress visually

2
function progressUpdate() {
3
  const percent = (audioElement.currentTime / audioElement.duration) * 100
4
  progressFilled.style.flexBasis = `${percent}%`
5
}

 Add Volume Controls

Adjusting volume taps back into the AudioContext object we used before. We’ll need to call a method named createGain() and change the gain value to map to the volume range input within the HTML.

1
// Bridge the gap between gainNode and AudioContext so we can manipulate volume (gain)

2
const gainNode = audioCtx.createGain()
3
4
volumeControl.addEventListener("change", () => {
5
  gainNode.gain.value = volumeControl.value
6
})
7
8
track.connect(gainNode).connect(audioCtx.destination)

We created a track variable early on in this tutorial and are finally putting it to use here. Using the connect() method, you can connect the track to the gainNode and then to the AudioContext. Without this line, the volume range input doesn’t know about the volume of the audio.

We’ll listen for a change event to map the volume relative to the gain.

What Happens When the Audio Ends?

We can reset the player after the audio ends so it can be ready for another listen should the end user want to start it over.

1
// if the track ends, reset the player

2
audioElement.addEventListener("ended", () => {
3
  playButton.dataset.playing = "false"
4
  pauseIcon.classList.add("hidden")
5
  playIcon.classList.remove("hidden")
6
  progressFilled.style.flexBasis = "0%"
7
  audioElement.currentTime = 0
8
  audioElement.duration = audioElement.duration
9
})

Here we toggle the play button icon from pause to play, set the data-playing attribute to false, reset the progress bar, and the audioElement’s currentTime and duration properties.

Scrubbing the Progress Bar to Skip and Rewind

Our progress bar is functional visually, but it would be more helpful if you could click anywhere on the timeline and adjust the current audio playback. We can achieve this with a series of event listeners and a new function.

1
// Scrub player timeline to skip forward and back on click for easier UX

2
let mousedown = false
3
4
function scrub(event) {
5
  const scrubTime =
6
    (event.offsetX / progress.offsetWidth) * audioElement.duration
7
  audioElement.currentTime = scrubTime
8
}
9
10
progress.addEventListener("click", scrub)
11
progress.addEventListener("mousemove", (e) => mousedown && scrub(e))
12
progress.addEventListener("mousedown", () => (mousedown = true))
13
progress.addEventListener("mouseup", () => (mousedown = false))

The scrub() function requires an event argument we listen for. In particular, the offsetX property allows us to pinpoint where a user clicked and make calculations relative to the audioElement’s properties.

Finally, we can listen on the progress bar itself for a set of events like click, mousemove, mousedown, and mouseup to adjust the audio element’s currentTime property.

4. Putting it All Together

The final JavaScript code is below. One thing to note is on the first-page load; I call the setTimes() function once again so we can get real-time displayed correctly before the user even starts manipulating the audio player.

1
// load sound via <audio> tag

2
const audioElement = document.querySelector("audio")
3
const audioCtx = new AudioContext()
4
const track = audioCtx.createMediaElementSource(audioElement)
5
6
// Player controls and attributes

7
const playButton = document.querySelector(".player-play-btn")
8
const playIcon = playButton.querySelector(".player-icon-play")
9
const pauseIcon = playButton.querySelector(".player-icon-pause")
10
const progress = document.querySelector(".player-progress")
11
const progressFilled = document.querySelector(".player-progress-filled")
12
const playerCurrentTime = document.querySelector(".player-time-current")
13
const playerDuration = document.querySelector(".player-time-duration")
14
const volumeControl = document.querySelector(".player-volume")
15
16
document.addEventListener("DOMContentLoaded", () => {
17
  // Set times after page load

18
  setTimes()
19
20
  // Update progress bar and time values as audio plays

21
  audioElement.addEventListener("timeupdate", () => {
22
    progressUpdate()
23
    setTimes()
24
  })
25
26
  // Play button toggle

27
  playButton.addEventListener("click", () => {
28
    // check if context is in suspended state (autoplay policy)

29
    // By default, browsers won't allow you to autoplay audio.

30
    // You can override by finding the AudioContext state and resuming it after a user interaction like a "click" event.

31
    if (audioCtx.state === "suspended") {
32
      audioCtx.resume()
33
    }
34
35
    // Play or pause track depending on state

36
    if (playButton.dataset.playing === "false") {
37
      audioElement.play()
38
39
      playButton.dataset.playing = "true"
40
      playIcon.classList.add("hidden")
41
      pauseIcon.classList.remove("hidden")
42
    } else if (playButton.dataset.playing === "true") {
43
      audioElement.pause()
44
      playButton.dataset.playing = "false"
45
      pauseIcon.classList.add("hidden")
46
      playIcon.classList.remove("hidden")
47
    }
48
  })
49
50
  // if the track ends, reset the player

51
  audioElement.addEventListener("ended", () => {
52
    playButton.dataset.playing = "false"
53
    pauseIcon.classList.add("hidden")
54
    playIcon.classList.remove("hidden")
55
    progressFilled.style.flexBasis = "0%"
56
    audioElement.currentTime = 0
57
    audioElement.duration = audioElement.duration
58
  })
59
60
  // Bridge the gap between gainNode and AudioContext so we can manipulate volume (gain)

61
  const gainNode = audioCtx.createGain()
62
  const volumeControl = document.querySelector(".player-volume")
63
  volumeControl.addEventListener("change", () => {
64
    gainNode.gain.value = volumeControl.value
65
  })
66
67
  track.connect(gainNode).connect(audioCtx.destination)
68
69
  // Display currentTime and duration properties in real-time

70
  function setTimes() {
71
    playerCurrentTime.textContent = new Date(audioElement.currentTime * 1000)
72
      .toISOString()
73
      .substr(11, 8)
74
    playerDuration.textContent = new Date(audioElement.duration * 1000)
75
      .toISOString()
76
      .substr(11, 8)
77
  }
78
79
  // Update player timeline progress visually

80
  function progressUpdate() {
81
    const percent = (audioElement.currentTime / audioElement.duration) * 100
82
    progressFilled.style.flexBasis = `${percent}%`
83
  }
84
85
  // Scrub player timeline to skip forward and back on click for easier UX

86
  let mousedown = false
87
88
  function scrub(event) {
89
    const scrubTime =
90
      (event.offsetX / progress.offsetWidth) * audioElement.duration
91
    audioElement.currentTime = scrubTime
92
  }
93
94
  progress.addEventListener("click", scrub)
95
  progress.addEventListener("mousemove", (e) => mousedown && scrub(e))
96
  progress.addEventListener("mousedown", () => (mousedown = true))
97
  progress.addEventListener("mouseup", () => (mousedown = false))
98
99
  // Track credit: Outfoxing the Fox by Kevin MacLeod under Creative Commons + MDN for the link.

100
})

Conclusion

There you have it! With a bit of JavaScript and elbow grease, you can create your very own branded music player.

From here, you might experiment with adding more controls, like skipping buttons or panning buttons. I’d also check out the AudioTracklist interface, which allows you to create playlists and extend the design as necessary.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.