Dec 16

Audio API

Review the audio element and overview some of what's possible with the audio API by building a custom audio player.

By Stephanie Eckles

While you're not likely to be embedding audio on the web every day, you never know when it might be useful!

The API is vast, and complements the <audio> native HTML element. We'll review the element first, then look at a few parts of the API to build a simple custom player.

The audio HTML element

The <audio> HTML element allows you to embed audio anywhere, and adding the controls attribute also displays the browser's audio player.

<audio controls src="jingle-bells.mp3"></audio>

The results of that HTML will display a little differently depending on your browser.

Audio converted and edited from the Jingle Bells midi.

Additional available attributes outside of controls include autoplay, loop, and muted. You can also include <source> elements as child elements of <audio> instead of using the src attribute.

You may also define what, if anything, to preload, where the default is different per browser. The option selected for preloading depends on how you intend to use the audio element Options include none, metadata, and auto.

Creating a custom audio player

The primary issue with the native player is that there is no cross-browser styling control over its elements. And each browser player is also inconsistent in which controls they provide.

For our custom player, we're using a reduced scope since this is intended as an introduction to this API. There are certainly more features you could add, but we'll focus on:

  1. Displaying track title (static)
  2. Duration and time elapsed
  3. Play/pause state toggle
  4. Mute/unmute state toggle

Audio player HTML

First, we'll build out semantic, accessible HTML for our custom audio player. I encourage always starting with this step (yes, even if you're using a framework).

We'll create a container with the role of "group" to help define the boundaries of our custom player for assistive technology (AT), as well as an aria-label to label the group "Audio player". And, we'll go ahead and add an <audio> element as the first child. Note that we're specifically defining the preload here since we are planning to use the metadata for display.

<div role="group" aria-label="Audio player" class="audio-player">
<audio preload="metadata" src="jingle-bells.mp3"></audio>
</div>

Since we haven't added the controls attribute, browsers will default to hiding the audio element.

Next, we'll use a headline element to label what is currently playing:

<h3 class="audio-player__current"><small>Playing:</small> Jingle Bells</h3>

Following that, we'll add two button elements to use as our player toggles for play and mute actions. We're wrapping the text in a span which we'll accessibly hide so that it is used for the label by AT. The icon svg include aria-hidden="true" since they are decorative as far as AT is concerned given the accessible labeling text.

<button data-action="play" data-state="paused" class="audio-player__button" type="button">
<span class="audio-player__label">Play</span>
<svg data-play height="24" width="24" aria-hidden="true">
<path d="M8 5v14l11-7z" />
</svg>
<svg data-pause hidden height="24" width="24">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
</button>
<button data-action="mute" class="audio-player__button" type="button" aria-pressed="false">
<span class="audio-player__label">Mute</span>
<svg height="24" width="24" aria-hidden="true">
<!-- svg path -->
</svg>
</button>

There are two SVG icons for a "play" and "pause" icon, where the "pause" initially has the hidden attribute so that it is not displayed. We'll toggle the icons' hidden attribute as well as the label content when it is selected.

The other important attributes on the buttons are:

Finally, we'll use a paragraph to contain the time information and set default zero values. Here again, we're preparing our markup to hide the AT labels, so the final visual will be 0:00 / 0:00.

<p class="audio-player__time">
<span class="audio-player__label">Elapsed time&nbsp;</span><span data-time="remaining">0:00</span> / <span class="audio-player__label"> Total time&nbsp;</span><span data-time="total">0:00</span>
</p>

Audio player styles

Since our audio player is completely made of custom elements now, we have full styling control. We'll be leveraging CSS grid to arrange the elements.

You can expand the styles to review them fully, and lookout for a few key features:

Audio player CSS
.audio-player * {
margin: 0;
}

.audio-player__label {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}

.audio-player {
padding: 1rem;
background: #222;
color: #fff;
border-radius: 0.5rem;
display: grid;
grid-template-columns: auto auto 1fr;
align-items: center;
width: fit-content;
gap: 1rem;
}

.audio-player__current {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 0.25em;
font-size: 1.25rem;
font-weight: bold;
}

.audio-player__current small {
font-weight: normal;
text-transform: uppercase;
}

.audio-player__button {
all: unset;
cursor: pointer;
display: inline-grid;
place-content: center;
border-radius: 0.25rem;
border: 2px solid #fff;
padding: 0.15em;
}

.audio-player__button:focus {
outline: 2px solid #fff;
outline-offset: 2px;
}

.audio-player__button svg {
fill: currentcolor;
pointer-events: none;
}

.audio-player__button[aria-pressed=true],
.audio-player__button[data-state=playing]
{
background-color: #fff;
color: #222;
}

.audio-player__time {
font-variant-numeric: tabular-nums;
}

Playing: Jingle Bells

Elapsed time 0:00 / Total time 0:00

Audio player API events

Both the audio and video API inherit the HTMLMediaElement interface. There are many properties, methods, and events that you can use. We'll hone in on some of the most essential for our simple player.

The capabilities we need for our audio player:

First, we'll find each element, including the <audio> since we directly respond to events and set properties on it.

const audioElement = document.querySelector(".audio-player audio");
const audioPlay = document.querySelector('[data-action="play"]');
const playLabel = audioPlay.querySelector('.audio-player__label');
const playIcon = audioPlay.querySelector('[data-play]');
const pauseIcon = audioPlay.querySelector('[data-pause]');
const audioMute = document.querySelector('[data-action="mute"]');
const audioTimeRemaining = document.querySelector('[data-time="remaining"]');
const audioTimeTotal = document.querySelector('[data-time="total"]');

Our play button functionality is the most complex. For setting play versus pause, we call those functions against the audio element. Additionally, we update the data-state which is tied to styles, as well as change the text content of the label. Last, we use the handle toggleAttribute JS function to toggle the hidden attribute of the icons to efficiently flip their visibility.

audioPlay.addEventListener("click", (e) => {
const isPlaying = audioPlay.dataset.state === "playing";

if (isPlaying) {
audioElement.pause();
audioPlay.dataset.state = 'paused';
playLabel.textContent = "Play";
} else {
audioElement.play();
audioPlay.dataset.state = 'playing';
playLabel.textContent = "Pause";
}

playIcon.toggleAttribute('hidden');
pauseIcon.toggleAttribute('hidden');
});

For mute, we toggle the boolean muted property and aria-pressed state.

audioMute.addEventListener("click", (e) => {
const isMuted = audioMute.getAttribute("aria-pressed") === "true";
audioMute.setAttribute("aria-pressed", !isMuted);
audioElement.muted = !isMuted;
});

Now that the toggles are working, it's time to handle the time updates. There are three events we'll listen to:

  1. loadedmetadata: allows us to confidently set the total time by getting the duration
  2. timeupdate: fires during playback and we'll use it to update the elapsed time
  3. ended: signals the audio file has completed playback which we'll use to reset the player state

Since the time is calculated in seconds, we'll first create a function to format it to mm:ss for display.

const formatAudioTime = (time) => {
if (isNaN(time)) return;
const minuteValue = Math.floor(time / 60);
let secondValue = Math.floor(time - minuteValue * 60);
if (secondValue < 10) {
secondValue = "0" + secondValue;
}
return minuteValue + ":" + secondValue;
};

Then, we'll call the formatting function for setting the total time upon the loadedmetadata event and setting the time remaining on the timeupdate event.

audioElement.addEventListener( "loadedmetadata", () => 
(audioTimeTotal.textContent = formatAudioTime(audioElement.duration))
);

audioElement.addEventListener( "timeupdate", () =>
(audioTimeRemaining.textContent = formatAudioTime(audioElement.currentTime))
);

Finally, we'll complete our player by resetting the play button state and remaining time upon the ended event.

audioElement.addEventListener("ended", () => {
audioPlay.dataset.state = 'paused';
playLabel.textContent = "Play";
playIcon.toggleAttribute('hidden');
pauseIcon.toggleAttribute('hidden');
audioTimeRemaining.textContent = "0:00";
});

The following is our final demo as well as the complete script.

Audio player JS
// .playable scoping due to previous CSS demo
// ⚡️ Challenge: Adapt this to work for multiple players per page
const audioElement = document.querySelector(".playable.audio-player audio");
const audioPlay = document.querySelector('.playable [data-action="play"]');
const playLabel = audioPlay.querySelector('.audio-player__label');
const playIcon = audioPlay.querySelector('[data-play]');
const pauseIcon = audioPlay.querySelector('[data-pause]');
const audioMute = document.querySelector('.playable [data-action="mute"]');
const audioTimeRemaining = document.querySelector('.playable [data-time="remaining"]');
const audioTimeTotal = document.querySelector('.playable [data-time="total"]');

audioPlay.addEventListener("click", (e) => {
const isPlaying = audioPlay.dataset.state === "playing";

if (isPlaying) {
audioElement.pause();
audioPlay.dataset.state = 'paused';
playLabel.textContent = "Play";
} else {
audioElement.play();
audioPlay.dataset.state = 'playing';
playLabel.textContent = "Pause";
}

playIcon.toggleAttribute('hidden');
pauseIcon.toggleAttribute('hidden');
});

audioMute.addEventListener("click", (e) => {
const isMuted = audioMute.getAttribute("aria-pressed") === "true";

audioMute.setAttribute("aria-pressed", !isMuted);
audioElement.muted = !isMuted;
});

const formatAudioTime = (time) => {
if (isNaN(time)) return;

const minuteValue = Math.floor(time / 60);
let secondValue = Math.floor(time - minuteValue * 60);

if (secondValue < 10) {
secondValue = "0" + secondValue;
}

return minuteValue + ":" + secondValue;
};

audioElement.addEventListener( "loadedmetadata", () =>
(audioTimeTotal.textContent = formatAudioTime(audioElement.duration))
);

audioElement.addEventListener( "timeupdate", () =>
(audioTimeRemaining.textContent = formatAudioTime(audioElement.currentTime))
);

audioElement.addEventListener("ended", () => {
audioPlay.dataset.state = 'paused';
playLabel.textContent = "Play";
playIcon.toggleAttribute('hidden');
pauseIcon.toggleAttribute('hidden');
audioTimeRemaining.textContent = "0:00";
});

Playing: Jingle Bells

Elapsed time 0:00 / Total time 0:00

Ideas for using the audio API

Additional resources