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.
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.
- Chromium: play/pause, duration and time elapsed, timeline scrubber, volume, and additional menu to download or select playback speed
- Firefox: play/pause, duration and time elapsed, timeline scrubber, and volume
- Safari: play/pause, timeline scrubber, and time elapsed
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:
- Displaying track title (static)
- Duration and time elapsed
- Play/pause state toggle
- 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:
data-action
: used for finding the element in JS. We're avoiding an ID in case of multiple audio players and avoiding a class to discourage removing or changing the namearia-pressed
: identifies the mute button as a toggle button for AT and will convey the changed state which will update dynamically in JS
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 </span><span data-time="remaining">0:00</span> / <span class="audio-player__label"> Total time </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:
- We hide our labels using the styles from Scott O'Hara's visually hidden class
- On the
.audio-player
container, we setwidth
usingfit-content
which visually shrinks the player to its intrinsic width where supported - For the toggle buttons SVG, we set
pointer-events: none
so that the click event "falls through" to the button element - The time content has set
font-variant-numeric: tabular-nums
which, when supported by the font, requests that numeric values take on monospace behavior, which prevents our audio player's width visibly adjusting when wider or narrower numbers are displayed
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:
- Toggle the aria-pressed state for the play and mute action buttons
- Toggle play state
- Toggle mute state
- Set the total duration
- Update the elapsed timestamp during playback
- Reset the elapsed timestamp and play toggle state when the audio ends
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:
loadedmetadata
: allows us to confidently set the total time by getting the durationtimeupdate
: fires during playback and we'll use it to update the elapsed timeended
: 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
#- Audio elements with no controls can be used to help respond to
play()
commands to add fun audio enhancements. Click Jason's head for inspiration. Be sure to also provide the user a toggle to turn off all sounds. - Create a more full-featured player including visualizations
- Use the API to aid animation like Andrew Rubin does to animate a cartoon band
- Have a podcast? Now you can create a custom player embed!
Additional resources
#- Learn with Jason from Lindsey Kopacz to create an accessible audio player
- MDN has a large section with many guides to explore the audio API
- Check out what all is available for the HTMLMediaElement (which applies to video, too)
- If you're wondering why we didn't use
aria-pressed
for the play button, learn from Sarah Higley about conveying the state of toggle buttons to AT