Building a Custom Audio Player with React + TypeScript - A Step-by-Step Tutorial
Learn how to build a fully functional audio player with customizable controls and interfaces
Introduction
When you're developing an application for music, podcasts, or any other application that involves loading and playing audio files, one of the initial challenges you encounter is figuring out how to create and customize your own audio player. This task can be quite daunting as it requires not only the technical knowledge to handle audio file formats and playback functionality but also the creative aspect of designing an appealing and user-friendly audio player interface. It may involve grappling with complex coding, integrating audio libraries or APIs, ensuring cross-platform compatibility, and dealing with potential issues such as buffering, latency, or audio quality. Additionally, you may need to consider user experience (UX) and user interface (UI) design principles to provide a seamless and visually appealing audio playback experience for your application's users.
In this tutorial, we will learn how to create a custom audio player using ReactPlayer, a popular React library for embedding audio and video in web applications. Custom audio player allows developers to design and implement their own user interface for audio playback, giving them more flexibility and control over the look and feel of their audio player. We will explore how to create custom play/pause buttons, volume controls, seek bars, durations for elapsed time and time left, muting, and looping audio, using React components and ReactPlayer's built-in props and event handlers.
To focus on the implementation part of the application, we will be using Tailwind CSS to add styles to our application. This should not be a limitation if you're on a different CSS framework, you can apply knowledge from the tutorial with plain CSS or any CSS framework of your choice.
Prerequisites
Basic knowledge of React and React Hooks
Basic knowledge of TypeScript
Familiarity with Tailwind is a plus but not required
Basic knowledge of Git, and package managers (npm or yarn)
Let's get started
Installation
Cloning the starter app
To get started, I have provided a starter app on GitHub. The application has been configured with Create React App, TypeScript, React icons, and Tailwind CSS. Open your terminal and run the following command:
git clone -b starter https://github.com/Cradoe/audio-player-tutorial.git
The above command will clone a copy of the starter app in the audio-player-tutorial directory. Run the following command, first, to go inside our application's directory and install the dependencies
cd audio-player-tutorial && npm install
or if you prefer yarn, use
cd audio-player-tutorial && yarn install
The cloned application has the structure that we need for our application. The files are literally empty but don't worry, we'll take them one after the other. And if you feel the need to restructure based on your project setup, that's okay, feel free.
Installing React Player
Next, let's install React Player, which is required for our audio player. Use the following command to install it:
npm install react-player
or
yarn install react-player
Once you have installed React Player, there are no further dependencies to install.
To start the application, run
npm start
or
yarn start
If all goes well, you should see an interface that says welcome.
Now, let's dive deep into our player component and start building our custom audio player.
The AudioPlayer component
Open the AudioPlayer.tsx
file and paste the following code:
// components/AudioPlayer.tsx
import ReactPlayer from "react-player";
import { useRef } from "react";
type Props = {
url: string;
title: string;
author: string;
thumbnail: string;
};
export const AudioPlayer = ({ url, title, author, thumbnail }: Props) => {
const playerRef = useRef<ReactPlayer | null>(null);
return (
<div>
<ReactPlayer
ref={playerRef}
url={url}
/>
</div>
);
};
The code above serves as the foundation of our AudioPlayer
component. We have defined the basic props that will be passed down from its parent component, in our case, the App.tsx
. The component is then rendered with a ref
prop set to playerRef
, which allows us to reference the ReactPlayer
component instance that is rendered in the DOM. This enables us to access and manipulate the ReactPlayer
component directly using the playerRef
object for further customization and functionality.
Now, let's update our App.tsx
file to use the AudioPlayer
component. Replace the existing code in App.tsx
with the following:
// App.tsx
import { AudioPlayer } from "./components/AudioPlayer";
const audio = {
url: "https://storage.googleapis.com/media-session/elephants-dream/the-wires.mp3",
title: "A sample audio title",
author: "The Elephants Dream",
thumbnail:
"https://images.unsplash.com/photo-1511379938547-c1f69419868d",
};
const App = ()=> {
return (
<div className="container mx-auto text-center">
<div className="md:w-1/2 lg:w-1/3 mx-auto">
<AudioPlayer
url={audio.url}
title={audio.title}
author={audio.author}
thumbnail={audio.thumbnail}
/>
</div>
</div>
);
}
export default App;
In the updated code, we have defined a sample object for our audio and passed its properties as props to our AudioPlayer
component. This will render the AudioPlayer
component in our app with the provided audio data.
To fully implement and control our audio player, we need to define some states and event handlers in our AudioPlayer.tsx
file.
First, update your import to include useState
// components/AudioPlayer.tsx
import { useRef, useState } from "react";
States
Let's define our states
// components/AudioPlayer.tsx
const [playing, setPlaying] = useState<boolean>(false);
const [muted, setMuted] = useState<boolean>(false);
const [volume, setVolume] = useState<number>(0.5);
const [progress, setProgress] = useState<number>(0);
const [loop, setLoop] = useState<boolean>(false);
const [duration, setDuration] = useState<number>(0);
Add the above code immediately after the line where we defined our playerRef
. We'll use each of these states to control the behavior and functionality of our audio player.
Event handlers
To manage events from our custom controls and the ReactPlayer
component, let's define the following event handlers:
// components/AudioPlayer.tsx
// event handlers for custom controls
const handlePlay = () => {
setPlaying(true);
};
const handlePause = () => {
setPlaying(false);
};
const handleVolumeChange = (newVolume: number) => {
setVolume(newVolume);
};
const toggleMute = () => {
setMuted((prevMuted) => !prevMuted);
};
const handleProgress = (state: any) => {
setProgress(state.played);
};
const handleDuration = (duration: number) => {
setDuration(duration);
};
const toggleLoop = () => {
setLoop((prevLoop) => !prevLoop);
};
These event handlers will handle various events triggered by the ReactPlayer
component and custom controls in our audio player.
handlePlay and handlePause: These functions toggle the
playing
state of our player, allowing us to control when the audio plays and when it pauses.handleVolumeChange: This function, which takes
newVolume
as a parameter, allows us to listen to changes in volume and adjust the player's volume accordingly based on the value received from our custom controls.toggleMute: This function allows us to control the muting and unmuting of our audio player by toggling the value of the
muted
state.onProgress: This function is bound to the
onProgress
event of theReactPlayer
component and is fired every time the player progresses in its playing mode. It accepts the state of the player, which can be used later in our custom controls to update the UI.handleDuration: This function is bound to the
onDuration
event of the ReactPlayer component and accepts a parameterduration
, which allows us to get details about the total elapsed time and time remaining for our audio. This information can be used to display the time left and elapsed time on the UI.toggleLoop: This function toggles the value of our
loop
state, allowing users to control whether they want to loop over the same audio or not.
Binding states and event handlers to ReactPlayer
After we are done defining the states and event handlers that we would need, let's bind them the our ReactPlayer
component. This would make it possible for us to manipulate the states and also actively listen for events from React Player. Update your ReactPlayer
component with this:
// components/AudioPlayer.tsx
<ReactPlayer
ref={playerRef}
url={url}
playing={playing}
volume={volume}
muted={muted}
loop={loop}
onPlay={handlePlay}
onPause={handlePause}
onProgress={handleProgress}
onDuration={handleDuration}
/>
AudioDetails component
To enhance the visual appearance of our AudioPlayer component, we can add styles to format how the title, thumbnail, and author of the song that is playing. In the AudioDetails.tsx
file, you can use the following code:
// components/AudioDetails.tsx
type Props = {
title: string;
author: string;
thumbnail: string;
};
export const AudioDetails = ({ title, author, thumbnail }: Props) => {
return (
<div className="bg-gray-800 rounded-t-xl px-5 py-8">
<div className="flex space-x-4">
<img
src={thumbnail}
alt=""
width="150"
height="150"
className="flex-none rounded-lg bg-gray-100"
/>
<div className="flex-1 w-2/3 space-y-3 grid justify-start">
<p className="text-gray-200 text-lg leading-6 font-semibold truncate w-auto">
{title}
</p>
<p className="text-cyan-500 text-sm leading-6 capitalize">
{author}
</p>
</div>
</div>
</div>
);
};
In the above code:
Props
title
,author
, andthumbnail
are defined as string type.Tailwind CSS classes are used to format the appearance of the component.
To use the AudioDetails
component in our AudioPlayer
component, first let's import AudioDetails
// components/AudioPlayer.tsx
import {AudioDetails} from "./AudioDetails";
Then we can add the following code immediately after the ReactPlayer
component in the AudioPlayer.tsx
file:
// components/AudioPlayer.tsx
<div className="shadow rounded-xl">
<AudioDetails title={title} author={author} thumbnail={thumbnail} />
</div>
PlayerControls component
Now let's define our custom controls and adjust the interface to meet our UI requirements. We'll be using the react-icons package as our icon library for most of our control buttons.
Component definition
Copy and paste the following code into the PlayerControls.tsx
file:
// components/PlayerControls.tsx
import { useRef, useState, useMemo } from "react";
type Props = {
playerRef: any;
playing: boolean;
loop: boolean;
volume: number;
muted: boolean;
progress: number;
duration: number;
handlePlay: () => void;
toggleMute: () => void;
toggleLoop: () => void;
handlePause: () => void;
handleVolumeChange: (newVolume: number) => void;
};
export const PlayerControls = ({
playerRef,
loop,
playing,
volume,
muted,
progress,
duration,
handlePlay,
toggleLoop,
handlePause,
handleVolumeChange,
toggleMute,
}: Props) => {
const [played, setPlayed] = useState<number>(0);
const [seeking, setSeeking] = useState<boolean>(false);
const playPauseButtonRef = useRef<HTMLButtonElement>(null);
return (
<div className="bg-gray-50 rounded-b-xl py-10">
</div>
);
};
In the above code:
Props
playerRef
,loop
,playing
,volume
,muted
,progress
, andduration
are defined with their respective types.Function handlers such as
handlePlay
,handlePause
,toggleMute
,toggleLoop
, andhandleVolumeChange
are passed as props.States for
played
andseeking
(jumping to a specific time point within the audio file) are defined usinguseState
hook.A
ref
is created usinguseRef
hook for the play/pause button element.
Next, let's add the following codes to our PlayerControls
component, right before the return statement:
Play and Pause
// components/PlayerControls.tsx
const togglePlayAndPause = () => {
if (playing) {
handlePause();
} else {
handlePlay();
}
};
Seeking
// components/PlayerControls.tsx
const handleSeekMouseDown = (e: any) => {
setSeeking(true);
};
const handleSeekChange = (e: any) => {
setPlayed(parseFloat(e.target.value));
};
const handleSeekMouseUp = (e: any) => {
playerRef.current?.seekTo(parseFloat(e.target.value));
setSeeking(false);
};
Volume
// components/PlayerControls.tsx
const handleChangeInVolume = (e: React.ChangeEvent<HTMLInputElement>) => {
handleVolumeChange(Number(e.target.value));
};
Progress
// components/PlayerControls.tsx
useMemo(() => {
setPlayed((prevPlayed) => {
if (!seeking && prevPlayed !== progress) {
return progress;
}
return prevPlayed;
});
}, [progress, seeking]);
In the above code:
togglePlayAndPause function is defined to toggle between play and pause based on the
playing
prop passed to the component.handleSeekMouseDown, handleSeekChange, and handleSeekMouseUp functions are defined to handle
seeking
behavior when the user interacts with the seek bar.handleChangeInVolume function is defined to handle volume change when the user interacts with the volume control.
useMemo hook is used to optimize the update of the
played
state based on the progress prop passed to the component, and this happens only when the user is not in the process ofseeking
and the newprogress
value is not the same as the previous value.
Let's import some icons that we would use in our JSX.
// components/PlayerControls.tsx
import { CiPlay1, CiPause1 } from "react-icons/ci";
import { VscMute, VscUnmute } from "react-icons/vsc";
import { ImLoop } from "react-icons/im";
Also, let's update our JSX as follows:
// components/PlayerControls.tsx
<div className="bg-gray-50 rounded-b-xl py-10">
<div className="mb-8 flex gap-x-10 px-10">
{/* duration: time played */}
<div className="text-xs text-gray-600">
{/* <Duration seconds={duration * played} /> */}
</div>
{/* progress bar */}
<div className="flex-1 mx-auto">
<input
type="range"
min={0}
max={0.999999}
step="any"
value={played}
onMouseDown={handleSeekMouseDown}
onChange={handleSeekChange}
onMouseUp={handleSeekMouseUp}
className="w-full h-4 rounded-lg appearance-none bg-slate-400 accent-gray-900 focus:outline focus:outline-cyan-500 "
/>
</div>
{/* duration: time left */}
<div className="text-xs text-gray-600 flex">
-{/* <Duration seconds={duration * (1 - played)} /> */}
</div>
</div>
<div className="grid grid-cols-3 items-center ">
{/* loop button */}
<div className="flex justify-center">
<button
className={`focus:outline focus:outline-cyan-500 font-bold hover:bg-gray-200 ${
loop && "text-cyan-500"
}`}
onClick={toggleLoop}
>
<ImLoop />
</button>
</div>
{/* play/pause button */}
<div className="flex justify-center">
<button
ref={playPauseButtonRef}
className="focus:outline focus:outline-cyan-500 border border-cyan-500 rounded-md p-4 hover:bg-gray-200"
onClick={togglePlayAndPause}
>
{playing ? <CiPause1 /> : <CiPlay1 />}
</button>
</div>
{/* volume control */}
<div className="flex justify-center items-center gap-1">
{/* mute button */}
<button
className="focus:outline focus:outline-cyan-500"
onClick={toggleMute}
>
{muted ? <VscMute /> : <VscUnmute />}
</button>
{/* volume slider */}
<input
type="range"
className="focus:outline focus:outline-cyan-500 w-[50%] h-2 rounded-lg bg-slate-400 accent-gray-900"
min={0}
max={1}
step={0.1}
value={volume}
onChange={handleChangeInVolume}
/>
</div>
</div>
</div>
We made some updates to the JSX code by adding comments to indicate the start of each control element. We also defined functions to handle the controls, which are passed down from the AudioPlayer component.
Looping
Our updated JSX code has a button that calls the toggleLoop
method. This method was passed to PlayerControls
component, and it instructs the audio player to play the same audio over and over.
Mute
We also have a button in the updated JSX code that toggles muting. Users can use this to mute and unmute the player.
You must have noticed that we commented our durations, that's because we have decided to put that in another component.
Duration
Open up components -> Durations.tsx and paste the following code:
// components/Durations.tsx
export const Duration = ({ seconds }: { seconds: number }) => {
return (
<time dateTime={`P${Math.round(seconds)}S`}>{formatTime(seconds)}</time>
);
};
const formatTime = (seconds: number) => {
const date = new Date(seconds * 1000);
const hh = date.getUTCHours();
const mm = date.getUTCMinutes();
const ss = padString(date.getUTCSeconds());
if (hh) {
return `${hh}:${padString(mm)}:${ss}`;
}
return `${mm}:${ss}`;
};
const padString = (string: number) => {
return ("0" + string).slice(-2);
};
The code provided defines three functions that are used in the implementation of a Duration component, which displays the duration of an audio track in a human-readable format. It utilizes three functions: formatTime, padString, and Duration. The formatTime
function converts the duration
from seconds to hours, minutes, and seconds, while the padString
function adds leading zeros to single-digit numbers.
Now, we can go back to our components -> PlayerControls.tsx file to import Duration and uncomment the following
// components/PlayerControls.tsx
import { Duration } from "./Duration";
// ...
{/* duration: time played */}
<div className="text-xs text-gray-600">
<Duration seconds={duration * played} />
</div>
// ...
{/* duration: time left */}
<div className="text-xs text-gray-600 flex">
<Duration seconds={duration * (1 - played)} />
</div>
The Duration component is now imported and used to display the duration of the audio track in two places in the JSX code.
Oh Right! All our custom controls and interfaces are ready. But before we go, let's add a quick accessibility feature to our PlayerControls component. We would shift focus to the play button once the component is loaded, this would allow users to easily play the audio by pressing the Enter or Return keys.
Remember we defined const playPauseButtonRef = useRef<HTMLButtonElement>(null);
earlier? Now, add the following code right before the return statement in our component.
// components/PlayerControls.tsx
// update your import to have useEffect
import { useEffect, useMemo, useRef, useState } from "react";
// ...
const PlayerControls = ({
// props
}) => {
// ... our states and functions
// shifts focus to play button on component mount
useEffect(() => {
playPauseButtonRef.current?.focus();
}, []);
// ... rest of the component code ...
return (
// ... JSX code
);
};
The useEffect
hook is used to shift the focus to the play button (playPauseButtonRef
) once the PlayerControls
component is mounted. This allows users to easily play the audio by pressing the Enter or Return key after the component is loaded.
Importing and using the Player Controls Update the JSX of our AudioPlayer
component as follows:
// components/AudioPlayer.tsx
import { PlayerControls } from "./PlayerControls";
// components/AudioPlayer.tsx
// update the jsx
<div>
<ReactPlayer
ref={playerRef}
url={url}
playing={playing}
volume={volume}
muted={muted}
loop={loop}
onPlay={handlePlay}
onPause={handlePause}
onProgress={handleProgress}
onDuration={handleDuration}
/>
<div className="shadow rounded-xl">
<AudioDetails title={title} author={author} thumbnail={thumbnail} />
<PlayerControls
playerRef={playerRef}
playing={playing}
volume={volume}
muted={muted}
progress={progress}
duration={duration}
loop={loop}
// event handler props
toggleMute={toggleMute}
handlePlay={handlePlay}
toggleLoop={toggleLoop}
handlePause={handlePause}
handleVolumeChange={handleVolumeChange}
/>
</div>
</div>
Recap of what we have:
ReactPlayer: This component is used for playing audio and video. It is given the following props:
ref
: A reference to the player instance for controlling playback.url
: The URL of the audio to be played.playing
: A boolean flag indicating whether the audio is currently playing or not.volume
: The volume level of the audio.muted
: A boolean flag indicating whether the audio is muted or not.loop
: A boolean flag indicating whether the audio should loop or not.onPlay
: An event handler function for when the audio starts playing.onPause
: An event handler function for when the audio is paused.onProgress
: An event handler function for tracking the progress of audio playback.onDuration
: An event handler function for getting the duration of the audio.
AudioDetails: This component displays details about the audio such as
title
,author
, andthumbnail
. It is given the respective props for these details.PlayerControls: This is the custom control component that provides buttons for controlling audio playback. It is given the following props:
playerRef
: A reference to the ReactPlayer instance for controlling playback.playing
: A boolean flag indicating whether the audio is currently playing or not.volume
: The volume level of the audio.muted
: A boolean flag indicating whether the audio is muted or not.progress
: The progress of the audio playback.duration
: The duration of the audio.loop
: A boolean flag indicating whether the audio should loop or not.toggleMute
: An event handler function for toggling the mute state of the audio.handlePlay
: An event handler function for playing the audio.toggleLoop
: An event handler function for toggling the loop state of the audio.handlePause
: An event handler function for pausing the audio.handleVolumeChange
: An event handler function for changing the volume of the audio.
The result
At this stage our component is ready, you should have an interface like the one below, feel free to use the controls.
Conclusion
In this tutorial, we successfully built a custom audio player using React and TypeScript. We started by cloning the starter project from GitHub and installing the necessary dependencies. We then created separate components for audio details and player controls and implemented their functionalities using React's useState as our state management and event-handling solution. We also added accessibility features for improved usability.
Throughout the process, we utilized various React concepts such as refs, props, state, and event handling, as well as TypeScript's type annotations for static typing and improved code reliability. We also leveraged popular libraries like ReactPlayer for audio playback.
By following this step-by-step guide, you can create a fully functional audio player with customizable controls and interfaces, allowing you to integrate audio playback seamlessly into your web applications. With further customization and enhancements, you can adapt this audio player to suit your specific project requirements and provide an enhanced audio experience for your users.
You can find the finished project on GitHub, which serves as a valuable resource for reference and further exploration.
Building a custom audio player in React with TypeScript can be a rewarding and valuable addition to your web development skills. It opens up possibilities for creating engaging and interactive audio-driven applications and empowers you with the ability to implement custom audio players tailored to your project needs. Happy coding!
References
These references provide links to React Player
package and the finished project on GitHub. They can be helpful for further learning, troubleshooting, and customization of your custom audio player.