Building an audio player in React to play sound or music
Tags: react, webdev
Published: 2023-02-16 18:43:54

**Written by [Ibadehin Mojeed](https://blog.logrocket.com/author/ibadehinmojeed/)✏️**

If you run a podcast or any other sound- or music-related web application, you may want to add an audio player to your project. This guide will teach you how to build an audio player from scratch with React. 

The audio player will offer control features like playing or pausing a track, adjusting volume, moving to the next or previous track, moving a progress slider, and so on. We will also customize it to look consistent across browsers. 

Jump ahead:

*   [Setting up the development environment](#setting-up-dev-environment)
*   [Creating the React audio player project structure](#creating-react-audio-player-project-structure)
*   [Rendering the `AudioPlayer`child components](#rendering-audio-player-child-components)
*   [Embedding sound content to play in our React project](#embedding-sound-content-play-react-project)
*   [Adding custom controls for the React music player](#adding-custom-controls-react-sound-player)
*   [The `play()` and `pause()` HTML audio methods](#play-pause-html-audio-methods)
*   [Displaying the current track data](#displaying-current-track-data)
*   [Configuring the progress bar and volume slider](#configuring-progress-bar-volume-slider)
*   [Displaying time progress and duration](#displaying-time-progress-duration)
*   [The`requestAnimationFrame` API](#requestanimationframe-api)
*   [Navigating tracks in the React audio player](#navigating-tracks-react-music-player)
*   [Adding the volume slider](#adding-volume-slider)
*   [Triggering the`onEnded` event](#triggering-onended-event)

Before beginning the tutorial, ensure you have [a basic understanding of React](https://ibaslogic.com/react-tutorial-for-beginners/). 

Below is a preview of the end product we will build together: ![Reach Audio Player Project End Product Showing Paused Podcast Episode. Colored Boxes And Numbers In Circles Indicate Different Components Of Sound Player](https://blog.logrocket.com/wp-content/uploads/2023/02/img1-React-audio-player-end-product.jpeg) 

You can [interact with the project](https://ibaslogic.github.io/react-audio-player/), and after that, get started!

## Setting up the development environment <a name="setting-up-dev-environment">

Let’s run the following command to set up a [development environment with `create-react-app`](https://blog.logrocket.com/getting-started-with-create-react-app-d93147444a27/):

```bash
npx create-react-app react-audio-player
```

Then do the following to start the project:

```bash
cd react-audio-player
npm start
```

## Creating the React audio player project structure <a name="creating-react-audio-player-project-structure">

As you can see in the image above, we can break down the user interface into four components. The number labels on the image correspond to the following component names:

1.  `AudioPlayer` — the parent/root component
2.  `DisplayTrack` — renders the audio content
3.  `Controls` — renders the audio controls and volume slider
4.  `ProgressBar` — renders the audio progress bar and time

With this in mind, we will create the component files in the `src/components` folder. Let’s ensure our project directory follows the following structure:

```javascript
react-audio-player
   ...
    ├── src
    │    ├── components
    │    │      ├── AudioPlayer.js
    │    │      ├── Controls.js
    │    │      ├── DisplayTrack.js
    │    │      └── ProgressBar.js     
    │    ├── data
    │    ├── styles
    │    │      └── index.css
    │    ├── index.js
    │   ...
```

Notice we also added a `data` folder and `styles/index.css` file in the `src` to hold the audio files and CSS, respectively. Copy the [`react-audio-player` project styles](https://github.com/Ibaslogic/react-audio-player/blob/main/src/styles/index.css) and paste them into the `styles/index.css` file. 

Next, in the `components/AudioPlayer.js` file, let’s render some simple text:

```javascript
const AudioPlayer = () => {
  return (
    <div className="audio-player">
      <div className="inner">Audio player content</div>
    </div>
  );
};
export default AudioPlayer;
```

After that, replace the content of the `src/index.js` file with the following:

```javascript
import React from 'react';
import ReactDOM from 'react-dom/client';
import AudioPlayer from './components/AudioPlayer';

import './styles/index.css';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <AudioPlayer />
  </React.StrictMode>
);
```

Save the files and see the content of the `AudioPlayer` component rendered in the browser.

## Rendering the `AudioPlayer` child components <a name="rendering-audio-player-child-components">

Starting with the `DisplayTrack` component. In the `components/DisplayTrack.js` file, let’s render some simple text:

```javascript
const DisplayTrack = () => {
  return <div>DisplayTrack content here</div>;
};
export default DisplayTrack;
```

Then, import the component in the `components/AudioPlayer.js` file:

```javascript
import DisplayTrack from './DisplayTrack';

const AudioPlayer = () => {
  return (
    <div className="audio-player">
      <div className="inner">
        <DisplayTrack />
      </div>
    </div>
  );
};
export default AudioPlayer;
```

Save the files to see the component text rendered in the frontend. 

Again, let’s render some text for the other child components — `Controls` and `ProgressBar`. After that, we will import and render them in the `components/AudioPlayer.js` file like so:

```javascript
import DisplayTrack from './DisplayTrack';
import Controls from './Controls';
import ProgressBar from './ProgressBar';

const AudioPlayer = () => {
  return (
    <div className="audio-player">
      <div className="inner">
        <DisplayTrack />
        <Controls />
        <ProgressBar />
      </div>
    </div>
  );
};
export default AudioPlayer;
```

Save all files and ensure the component's contents are rendered in the front end.

## Embedding sound content to play in our React project <a name="embedding-sound-content-play-react-project">

We will use the HTML `<audio>` element to embed audio in our project. This element has a `src` attribute to specify the audio URL and a `controls` attribute to display the browser’s specific controls for the audio, such as play, pause, and others:

```javascript
<audio src=" " controls />
```

### Fetching the audio files

We need audio files to work with to use the `<audio>` element. So, let’s create a `tracks.js` file in the `src/data` folder and [add audio tracks](https://github.com/Ibaslogic/react-audio-player/blob/main/src/data/tracks.js) as an array of objects like so:

```javascript
// audio files
import beautiful from './a_beautiful_day.mp3';

// audio thumbnails
import trinix from './trinix.jpeg';

export const tracks = [
  {
    title: 'Trinix ft Rushawn – Its a beautiful day',
    src: beautiful,
    author: 'Trinix ft Rushawn',
    thumbnail: trinix,
  },
  // ...
];
```

You can copy the [sample audio files from the project repo](https://github.com/Ibaslogic/react-audio-player/tree/main/src/data) and use them in the `src/data` folder.

### Rendering the audio player

Because we will need the audio data in multiple children components, we will import the playlist file in the `AudioPlayer` parent component. Then, we will create a state to handle the current audio track. For now, we will assign the first track from the playlist. 

The `components/AudioPlayer.js` file now looks like so:

```javascript
import { useState } from 'react';
import { tracks } from '../data/tracks';

// import components
import DisplayTrack from './DisplayTrack';
import Controls from './Controls';
import ProgressBar from './ProgressBar';

const AudioPlayer = () => {
  const [currentTrack, setCurrentTrack] = useState(tracks[0]);
  return (
    <div className="audio-player">
      <div className="inner">
        <DisplayTrack currentTrack={currentTrack} />
        <Controls />
        <ProgressBar />
      </div>
    </div>
  );
};
export default AudioPlayer;
```

In the code, we assigned the first track in the array to a state using the zero `[0]` index. Notice how we also passed the state to the `DisplayTrack` component. 

Let’s access the state from the `components/DisplayTrack.js` file and update the `src` attribute of the `<audio>` tag:

```javascript
const DisplayTrack = ({ currentTrack }) => {
  return (
    <div>
      <audio src={currentTrack.src} controls />
    </div>
  );
};
export default DisplayTrack;
```

If we save all files, the audio should render like so: ![Simple React Audio Player With Default Controls And Styling Rendered In Browser](https://blog.logrocket.com/wp-content/uploads/2023/02/img2-Simple-React-audio-player-default-controls.png) 

The appearance of the audio control will vary between browsers. Therefore, we will create a custom control to get a consistent look and feel across browsers and add additional features. 

Let’s start by removing the `controls` attribute from the `<audio>` tag to dismiss the browser’s default control. We should have the following:

```javascript
<audio src={currentTrack.src} />
```

Next, we’ll add some custom controls.

## Adding custom controls for the React sound player <a name="adding-custom-controls-react-sound-player">

First, we need control icons. Let’s get icons from [the React Icons library](https://blog.logrocket.com/react-icons-comprehensive-tutorial-examples/) by running the following command in our project:

```bash
npm install react-icons
```

After that, open the `components/Controls.js` file and render the control icons. We should then have the following:

```javascript
import { useState } from 'react';

// icons
import {
  IoPlayBackSharp,
  IoPlayForwardSharp,
  IoPlaySkipBackSharp,
  IoPlaySkipForwardSharp,
  IoPlaySharp,
  IoPauseSharp,
} from 'react-icons/io5';

const Controls = () => {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <div className="controls-wrapper">
      <div className="controls">
        <button>
          <IoPlaySkipBackSharp />
        </button>
        <button>
          <IoPlayBackSharp />
        </button>

        <button>
          {isPlaying ? <IoPauseSharp /> : <IoPlaySharp />}
        </button>
        <button>
          <IoPlayForwardSharp />
        </button>
        <button>
          <IoPlaySkipForwardSharp />
        </button>
      </div>
    </div>
  );
};

export default Controls;
```

In the code, be aware that we used [the`useState()` Hook] (https://blog.logrocket.com/a-guide-to-usestate-in-react-ecb9952e406c/) to render the play and pause icons dynamically. Let’s add the functionality to switch the icons when we click the button. 

Update the play/pause button to include [the `onClick` event handler](https://blog.logrocket.com/react-onclick-event-handlers-guide/) like so:

```javascript
<button onClick={togglePlayPause}>
  {isPlaying ? <IoPauseSharp /> : <IoPlaySharp />}
</button>
```

Then, add the following handler above the `return` statement to toggle the Boolean state value when the `togglePlayPause` button is clicked:

```javascript
const togglePlayPause = () => {
  setIsPlaying((prev) => !prev);
};
```

Save your files and ensure the play and pause icons toggle when you click their button.

## The `play()` and `pause()` HTML audio methods <a name="play-pause-html-audio-methods">

To play and pause an audio track, the `<audio>` element exposes the `play()` and `pause()` methods. To invoke these methods, we must have access to the `<audio>` element. In React, we use [the `useRef`Hook](https://blog.logrocket.com/accessing-previous-props-state-react-hooks/#useref-depth-overlooked-react-hook) for that. 

Because we will need this `<audio>` reference in multiple children components, we will create it inside their parent component. Open the `components/AudioPlayer.js` file and import the `useRef` Hook:

```javascript
import { useRef, useState } from 'react';
```

Then, initialize `useRef` above the `return` statement and pass it to the `DisplayTrack` and `Controls` components:

```javascript
const AudioPlayer = () => {
  const [currentTrack, setCurrentTrack] = useState(tracks[0]);

  // reference
  const audioRef = useRef();

  return (
    <div className="audio-player">
      <div className="inner">
        <DisplayTrack
          currentTrack={currentTrack}
          audioRef={audioRef}
        />
        <Controls audioRef={audioRef} />
        <ProgressBar />
      </div>
    </div>
  );
};
export default AudioPlayer;
```

In the `DisplayTrack` component, let’s access the reference and assign it to a `ref` attribute on the `<audio>` element:

```javascript
const DisplayTrack = ({ currentTrack, audioRef }) => {
  return (
    <div>
      <audio src={currentTrack.src} ref={audioRef} />
    </div>
  );
};
export default DisplayTrack;
```

If we save our files, we should now have access to the `<audio>` element’s methods and properties via the returned object for the `ref`. You can log the `audioRef` in the `AudioPlayer` component and see what it returns in the browser console:

```javascript
const audioRef = useRef();
console.log(audioRef);
```

Next, let’s open the `components/Controls.js` file and access the reference variable. Then, on its `current` object, we can invoke the `play()` and `pause()` methods:

```javascript
import { useState, useEffect } from 'react';

// icons here

const Controls = ({ audioRef }) => {
  // ...
  useEffect(() => {
    if (isPlaying) {
      audioRef.current.play();
    } else {
      audioRef.current.pause();
    }
  }, [isPlaying, audioRef]);

  return (
    // ...
  );
};

export default Controls;
```

The focus in the code above is [the`useEffect` Hook](https://blog.logrocket.com/using-react-useeffect-hook-lifecycle-methods/). In the Hook, we checked if the `isPlaying` state value is true or not to either play or pause the audio track. 

If we save the files and visit the front end, we should be able to play and pause the current audio track.

## Displaying the current track data <a name="displaying-current-track-data">

To render the current track title, author, and thumbnail, let’s open the `components/DisplayTrack.js` file and update the JSX to include the data:

```javascript
import { BsMusicNoteBeamed } from 'react-icons/bs';

const DisplayTrack = ({ currentTrack, audioRef }) => {
  return (
    <div>
      <audio src={currentTrack.src} ref={audioRef} />
      <div className="audio-info">
        <div className="audio-image">
          {currentTrack.thumbnail ? (
            <img src={currentTrack.thumbnail} alt="audio avatar" />
          ) : (
            <div className="icon-wrapper">
              <span className="audio-icon">
                <BsMusicNoteBeamed />
              </span>
            </div>
          )}
        </div>
        <div className="text">
          <p className="title">{currentTrack.title}</p>
          <p>{currentTrack.author}</p>
        </div>
      </div>
    </div>
  );
};
export default DisplayTrack;
```

In the code above, the focus is the `div` wrapper element below the `<audio>` tag. In this `div`, we started by checking whether a thumbnail image exists or not to display it or render a custom style. 

Notice we imported an icon from the `react-icons` library to style a default thumbnail. Afterward, we rendered the track title and the author's name right after the thumbnail. 

If we save the file, the result should look like so: ![React Audio Player With Custom Controls And Styling Including Track Image, Title, And Author](https://blog.logrocket.com/wp-content/uploads/2023/02/img3-React-audio-player-custom-controls.png)

## Configuring the progress bar and volume slider <a name="configuring-progress-bar-volume-slider">

To add these functionalities, we will use the HTML `input` range element. Starting with the progress bar, let’s open the `components/ProgressBar.js` file and update the JSX, so we have the following:

```javascript
const ProgressBar = () => {
  return (
    <div className="progress">
      <span className="time current">00:00</span>
      <input type="range" />
      <span className="time">03:34</span>
    </div>
  );
};

export default ProgressBar;
```

If we save the file and view the project, we will see the browser’s default input range. Creating a range element that is consistent across browsers requires a bit more work using CSS rules. 

For this, let’s create a `styles/customize-progress-bar.css` file, copy the [`react-audio-player`project's CSS rules](https://github.com/Ibaslogic/react-audio-player/blob/main/src/styles/customize-progress-bar.css), and paste them into the newly created CSS file. 

Next, import the CSS file inside the `src/index.js`:

```css
import './styles/index.css';
import './styles/customize-progress-bar.css';
```

Ensure you save all files. 

It is important to note that we have used a CSS variable in the CSS file to assign the progress width dynamically:

```css
input[type="range"] {
  --range-progress: 0;
  /* ... */
}

input[type="range"]::before {
  /* ... */
  background: #f50;
  width: var(--range-progress);
  /* ... */
}
```

We started with a value of zero. We will dynamically update this value based on the audio progress. For now, let’s temporarily modify the CSS variable value to be 50 percent like so:

```css
input[type="range"] {
  --range-progress: 50%;
  /* ... */
}
```

We will then have a progress bar that is styled up to 50 percent of the total length: ![Custom Styled React Audio Player With Track Progress Slider Bar Added, Indicated By Orange Arrow](https://blog.logrocket.com/wp-content/uploads/2023/02/img4-React-audio-player-progress-slider.png) Let’s return the value assigned to the CSS variable to `0`:

```css
input[type="range"] {
  --range-progress: 0;
  /* ... */
}
```

### Implementing controlled and uncontrolled inputs

In React, form inputs like the range can either be controlled or uncontrolled. With controlled inputs, the component manages the state, and we write a handler for every state update. With uncontrolled inputs, the DOM manages the state, and we use a ref to get input values from the DOM. 

We will learn to implement both controlled and uncontrolled inputs. We will implement the uncontrolled logic on the progress bar and a controlled logic on the volume slider. 

Back to the progress bar, we will get the reference to the input range. Because we will need this ref in multiple child components, we will initialize it in their parent component. 

Open the `components/AudioPlayer.js` file and initialize a ref above the `return` statement and pass it to the `<ProgressBar />`:

```javascript
const AudioPlayer = () => {
  // ...
  const progressBarRef = useRef();

return (
  <div className="audio-player">
    <div className="inner">
      <DisplayTrack
        currentTrack={currentTrack}
        audioRef={audioRef}
      />
      <Controls audioRef={audioRef} />
      <ProgressBar progressBarRef={progressBarRef} />
    </div>
  </div>
);
};
export default AudioPlayer;
```

To make our code short, we can rewrite the returned JSX so we have the following:

```javascript
return (
  <div className="audio-player">
    <div className="inner">
      <DisplayTrack {...{ currentTrack, audioRef }} />
      <Controls {...{ audioRef }} />
      <ProgressBar {...{ progressBarRef }} />
    </div>
  </div>
);
```

Let’s save our file, open the `components/ProgressBar.js` file, access the reference, and assign it to a `ref` attribute on the `<input />` element:

```javascript
const ProgressBar = ({ progressBarRef }) => {
  return (
    <div className="progress">
      <span className="time current">00:00</span>
      <input type="range" ref={progressBarRef} />
      <span className="time">03:34</span>
    </div>
  );
};

export default ProgressBar;
```

Now that we have a reference to the `<input />`, we will add an `onChange` event to get the input values at every point. We will also set the default value to `0` so the range thumb starts at the beginning. The component now looks like so:

```javascript
const ProgressBar = ({ progressBarRef }) => {
  const handleProgressChange = () => {
    console.log(progressBarRef.current.value);
  };

  return (
    <div className="progress">
      <span className="time current">00:00</span>
      <input
        type="range"
        ref={progressBarRef}
        defaultValue="0"
        onChange={handleProgressChange}
      />
      <span className="time">03:34</span>
    </div>
  );
};

export default ProgressBar;
```

When changing the progress bar, we will see the corresponding values in the browser console. 

We will now take this value and assign it to the `currentTime` property of the audio playback. This property returns the audio playback's current position using seconds as the unit of measurement. 

To get the `currentTime` property, we need the audio reference. Fortunately, we created it earlier in the `components/AudioPlayer.js` file. So, let’s pass it down to the `<ProgressBar />` component, so we have the following:

```javascript
<ProgressBar {...{ progressBarRef, audioRef }} />
```

After that, open the `components/ProgressBar.js` file and access the audio ref. We will then assign the progress bar value to the `currentTime` property:

```javascript
const ProgressBar = ({ progressBarRef, audioRef }) => {
  const handleProgressChange = () => {
    audioRef.current.currentTime = progressBarRef.current.value;
  };
  return (
    // ...
  );
};

export default ProgressBar;
```

Let’s save all files. When we change the progress bar, the audio playback jumps to the specified position. Be aware that the time is static for now and the range progress color is not progressing. We will get there!

## Displaying time progress and duration <a name="displaying-time-progress-duration">

We will start by creating states to hold the audio time. In the `components/AudioPlayer.js` file, add the states and pass the state variables to the `<ProgressBar />`:

```javascript
const AudioPlayer = () => {
  // ...
  const [timeProgress, setTimeProgress] = useState(0);
  const [duration, setDuration] = useState(0);

  // reference

  return (
    <div className="audio-player">
      <div className="inner">
        {/* ... */}
        <ProgressBar
          {...{ progressBarRef, audioRef, timeProgress, duration }}
        />
      </div>
    </div>
  );
};
export default AudioPlayer;
```

Save the file, grab the states from the `ProgressBar` component, and render them in the JSX:

```javascript
const ProgressBar = ({
  // ...
  timeProgress,
  duration,
}) => {
  // ...
  return (
    <div className="progress">
      <span className="time current">{timeProgress}</span>
      {/* ... */}
      <span className="time">{duration}</span>
    </div>
  );
};

export default ProgressBar;
```

### Updating audio time

Like the current time, as we saw earlier, we also have access to the track duration via the `duration` audio property. 

Starting with the duration, we want to get and display the track duration as soon as the audio metadata loads. We will use the `onloadedmetadata` event on the `<audio />`. 

Open the `components/DisplayTrack.js` file and include this event:

```javascript
const DisplayTrack = ({ currentTrack, audioRef }) => {

  const onLoadedMetadata = () => {
    console.log(audioRef.current.duration);
  };

  return (
    <div>
      <audio
        src={currentTrack.src}
        ref={audioRef}
        onLoadedMetadata={onLoadedMetadata}
      />
      {/* ... */}
    </div>
  );
};
```

If we save and open the console, we will see the duration time in seconds. 

Next, we will update the duration state variable with the audio duration and ensure the `<input>` progress bar‘s `max` attribute matches the duration. 

In the `AudioPlayer` component, let’s pass the `setDuration` and `progressBarRef` to the `<DisplayTrack />`:

```javascript
const AudioPlayer = () => {
  // ...

  return (
    <div className="audio-player">
      <div className="inner">
        <DisplayTrack
          {...{ currentTrack, audioRef, setDuration, progressBarRef }}
        />
        {/* ... */}
      </div>
    </div>
  );
};
export default AudioPlayer;
```

Then, in the `DisplayTrack` component, grab these props and update the `onloadedmetadata` handler:

```javascript
const DisplayTrack = ({
  // ...
  setDuration,
  progressBarRef,
}) => {
  const onLoadedMetadata = () => {
    const seconds = audioRef.current.duration;
    setDuration(seconds);
    progressBarRef.current.max = seconds;
  };

  return (
    // ...
  );
};
export default DisplayTrack;
```

Now, we should see the track duration displayed: ![Custom React Audio Player With Track Duration Displayed In Seconds, Indicated By Orange Arrow](https://blog.logrocket.com/wp-content/uploads/2023/02/img5-React-audio-player-track-duration-seconds.png) The track duration is currently displayed using only seconds as the unit, but it would be easier to read if it displayed both minutes and seconds. Let’s create a function that formats the time to the desired display.

### Formatting the audio time

In the `ProgressBar` component, add the following code above the `return` statement:

```javascript
const formatTime = (time) => {
  if (time && !isNaN(time)) {
    const minutes = Math.floor(time / 60);
    const formatMinutes =
      minutes < 10 ? `0${minutes}` : `${minutes}`;
    const seconds = Math.floor(time % 60);
    const formatSeconds =
      seconds < 10 ? `0${seconds}` : `${seconds}`;
    return `${formatMinutes}:${formatSeconds}`;
  }
  return '00:00';
};
```

The `formatTime` function takes a time argument in seconds and reformats it to minutes:seconds. We will then use the function in the JSX like so:

```javascript
return (
  <div className="progress">
    <span className="time current">{formatTime(timeProgress)}</span>
    {/* ... */}
    <span className="time">{formatTime(duration)}</span>
  </div>
);
```

Save the file and see the audio time that is formatted the way we want.

## The `requestAnimationFrame` API <a name="requestanimationframe-api">

We need a mechanism that tells the browser to redraw every update to display the current time and the range progress. While we may think of using `setInterval()`, `requestAnimationFrame` is much more efficient and smoother when we want to handle repeated animation. 

This API takes a callback that it calls whenever it kicks off. To trigger the API and tell the browser to update the screen, we may call it once the audio starts playing and the current time increases. 

Let’s open the `components/Controls.js` file and trigger this API once the playback is ongoing in the `useEffect` Hook:

```javascript
const playAnimationRef = useRef();

const repeat = () => {
  console.log('run');

  playAnimationRef.current = requestAnimationFrame(repeat);
};

useEffect(() => {
  if (isPlaying) {
    audioRef.current.play();
    playAnimationRef.current = requestAnimationFrame(repeat);
  } else {
    audioRef.current.pause();
    cancelAnimationFrame(playAnimationRef.current);
  }
}, [isPlaying, audioRef, repeat]);
```

The `requestAnimationFrame` returns the `request-id` that we have assigned to `playAnimationRef.current`. The ref will preserve the returned id over time. This id lets us cancel the request once we pause the playback. 

Notice how the callback recursively calls itself to repeat the animation. If we save the file and click the audio play button, we will see the repeat function repeatedly run in the browser console. 

We will also see a terminal warning that says the repeat function changes the `useEffect` dependencies on every render. Let’s memorize the definition of the callback by wrapping it in [a `useCallback` Hook](https://blog.logrocket.com/react-usememo-vs-usecallback-a-pragmatic-guide/). Let’s import the `useCallback` from React:

```javascript
import {
  //...
  useCallback,
} from 'react';
```

Then, update the `repeat` callback to the following:

```javascript
const repeat = useCallback(() => {
  console.log('run');

  playAnimationRef.current = requestAnimationFrame(repeat);
}, []);
```

Next, we will define the following functionality in that callback:

*   Update the `timeProgress` state variable with the current audio time
*   Ensure the progress bar progresses with the current audio time
*   Dynamically update the audio progress — i.e., styling the length of the played section

Let’s pass the `progressBarRef`, `duration` state variable, and `setTimeProgress` from the `AudioPlayer` component into the `Controls` component:

```javascript
const AudioPlayer = () => {
  // ...
  return (
    <div className="audio-player">
      <div className="inner">
        {/* ... */}
        <Controls
          {...{ audioRef, progressBarRef, duration, setTimeProgress }}
        />
        {/* ... */}
      </div>
    </div>
  );
};
export default AudioPlayer;
```

Next, let’s access the props inside the `Controls` component and update the `repeat` callback, so we have the following:

```javascript
const Controls = ({
  audioRef,
  progressBarRef,
  duration,
  setTimeProgress,
}) => {
  // ...

  const repeat = useCallback(() => {
    const currentTime = audioRef.current.currentTime;
    setTimeProgress(currentTime);
    progressBarRef.current.value = currentTime;
    progressBarRef.current.style.setProperty(
      '--range-progress',
      `${(progressBarRef.current.value / duration) * 100}%`
    );

    playAnimationRef.current = requestAnimationFrame(repeat);
  }, [audioRef, duration, progressBarRef, setTimeProgress]);

  // ...
  return (
    // ...
  );
};

export default Controls;
```

The focus is on the `repeat` callback. With the `setTimeProgress` updater function, we update the audio elapse time. We also assigned the current time to the range value so it progresses. 

Regarding the length of the played section, we grabbed the CSS variable we declared in the CSS file and dynamically update it. 

If we save the file and play the audio track, we should see the following behavior: ![User Shown Clicking Play On Audio Player And Moving Progress Bar Forward And Backward](https://blog.logrocket.com/wp-content/uploads/2023/02/img6-React-audio-player-changing-progress-bar.gif) 

Be aware that if we pause the audio, changing the progress bar will not reflect the animation till we press play again. This is because we called the `cancelAnimationFrame` in the pause condition to cancel it. We can remove the `cancelAnimationFrame` and move the `requestAnimationFrame` out of condition, so we have the following:

```javascript
useEffect(() => {
  if (isPlaying) {
    audioRef.current.play();
  } else {
    audioRef.current.pause();
  }
  playAnimationRef.current = requestAnimationFrame(repeat);
}, [isPlaying, audioRef, repeat]);
```

The project should now work as expected.

## Navigating tracks in the React music player <a name="navigating-tracks-react-music-player">

Let’s work on the next, previous, skip forward, and skip backward buttons. We‘ll start by attaching `onClick` events to them. In the `components/Controls.js` file, update the `buttons` in the JSX to include `onClick` events:

```javascript
return (
  <div className="controls-wrapper">
    <div className="controls">
      <button onClick={handlePrevious}>
        <IoPlaySkipBackSharp />
      </button>
      <button onClick={skipBackward}>
        <IoPlayBackSharp />
      </button>

      <button onClick={togglePlayPause}>
        {isPlaying ? <IoPauseSharp /> : <IoPlaySharp />}
      </button>
      <button onClick={skipForward}>
        <IoPlayForwardSharp />
      </button>
      <button onClick={handleNext}>
        <IoPlaySkipForwardSharp />
      </button>
    </div>
  </div>
);
```

Then, above the `return` statement, add their respective handler functions:

```javascript
const skipForward = () => {};

const skipBackward = () => {};

const handlePrevious = () => {};

const handleNext = () => {};
```

### Handling the next track

To handle the next and previous track, we need to know the index of the current playing track and have access to the playlist. In the `components/AudioPlayer.js` file, let’s initialize a state for the track index:

```javascript
const [trackIndex, setTrackIndex] = useState(0);
```

The `currentTrack` state should currently look like this:

```javascript
const [currentTrack, setCurrentTrack] = useState(tracks[0]);
```

Let’s update it to this instead:

```javascript
const [currentTrack, setCurrentTrack] = useState(tracks[trackIndex]);
```

Next, let’s pass the `tracks`, `trackIndex`, `setTrackIndex`, and `setCurrentTrack`, to `<Controls />` component:

```javascript
<Controls
  {...{
    // ...
    tracks,
    trackIndex,
    setTrackIndex,
    setCurrentTrack,
  }}
/>
```

We will access them from the `Controls` component as props and then update the `handleNext` handler, so we have the following:

```javascript
const handleNext = () => {
  if (trackIndex >= tracks.length - 1) {
    setTrackIndex(0);
    setCurrentTrack(tracks[0]);
  } else {
    setTrackIndex((prev) => prev + 1);
    setCurrentTrack(tracks[trackIndex + 1]);
  }
};
```

With the code above, whenever we click the “next” button, we check if we are in the last index — i.e., the last track. Then, we set the index to `0` — i.e., the first track — and reset the track to the first item in the playlist. Otherwise, we set the index and the track to the next one in the playlist. 

Save the file and ensure the “next” button works as expected.

### Handling previous track

Similar to the “next track” handler, let’s update the `handlePrevious` handler so we have the following:

```javascript
const handlePrevious = () => {
  if (trackIndex === 0) {
    let lastTrackIndex = tracks.length - 1;
    setTrackIndex(lastTrackIndex);
    setCurrentTrack(tracks[lastTrackIndex]);
  } else {
    setTrackIndex((prev) => prev - 1);
    setCurrentTrack(tracks[trackIndex - 1]);
  }
};
```

If the current track index is `0`, we reset the index and the track to the last item in the playlist. Otherwise, we set the index and the track to the previous one in the playlist.

### Skipping forward and backward

In the respective handlers for skipping forward and backward, we will access the current time from the audio ref and assign a numeric value in seconds:

```javascript
const skipForward = () => {
  audioRef.current.currentTime += 15;
};

const skipBackward = () => {
  audioRef.current.currentTime -= 15;
};
```

We have assigned 15 seconds of skipping time, as seen above. Save and ensure the control buttons work as expected.

## Adding the volume slider <a name="adding-volume-slider">

As we mentioned earlier, we will use the HTML input range element to let us control the volume via a slider. In the `Controls` component, let’s add the volume container element after the control container:

```javascript
return (
  <div className="controls-wrapper">
    <div className="controls">{/* ... */}</div>
    <div className="volume">
      <button>icons</button>
      <input type="range" min={0} max={100} />
    </div>
  </div>
);
```

We also mentioned that we would make this input slider a controlled input. 

Let’s start by initializing a state in the `Controls` component:

```javascript
const [volume, setVolume] = useState(60);
```

Then, update the input element to include a `value` and `onChange` props:

```javascript
<input
  type="range"
  min={0}
  max={100}
  value={volume}
  onChange={(e) => setVolume(e.target.value)}
/>
```

Next, we will attach the returned value of the `volume` state variable to the `volume` property of the audio. Let’s do that by adding the following code above the `return` statement inside the `Controls` component:

```javascript
useEffect(() => {
  if (audioRef) {
    audioRef.current.volume = volume / 100;
  }
}, [volume, audioRef]);
```

In this component, we have access to the audio reference. Because the max value of the audio’s volume property is 1, we divided ours `volume` state — whose max value is 100 — by 100 so they can be in sync. 

Save the file and ensure you can increase and reduce the audio volume.

### Styling the volume progress

Let’s capitalize on the `volume` state variable to style the volume progress dynamically. On the volume input element, let's add a `style` attribute to style the progress:

```javascript
<input
  // ...
  style={{
    background: `linear-gradient(to right, #f50 ${volume}%, #ccc ${volume}%)`,
  }}
/>
```

The code above styles the volume progress bar using the CSS linear gradient. As the `volume` state increases, the progress bar's orange background color will also match until it takes up the rest of the progress bar’s length. Remember to save the file and ensure it works!

### Setting the volume icons

In the `Controls` component, let’s start by initializing a state to handle when the volume is muted:

```javascript
const [muteVolume, setMuteVolume] = useState(false);
```

By default, we gave it a Boolean value of `false`. Next, let’s locate the following `button` within the volume container element:

```javascript
<button>icons</button>
```

Replace this with the following code:

```javascript
<button onClick={() => setMuteVolume((prev) => !prev)}>
  {muteVolume || volume < 5 ? (
    <IoMdVolumeOff />
  ) : volume < 40 ? (
    <IoMdVolumeLow />
  ) : (
    <IoMdVolumeHigh />
  )}
</button>
```

In the code above, we have attached an `onClick` event that toggles the `muteVolume` state. We also used conditions to render icons dynamically based on the value of the `volume` state variable. 

Before saving the file, let’s import the following `react-icons` components at the top of the file:

```javascript
import {
  IoMdVolumeHigh,
  IoMdVolumeOff,
  IoMdVolumeLow,
} from 'react-icons/io';
```

Finally, in the `useEffect` Hook, let's assign the Boolean `muteVolume` state to the `muted` property of the audio:

```javascript
useEffect(() => {
  if (audioRef) {
    audioRef.current.volume = volume / 100;
    audioRef.current.muted = muteVolume;
  }
}, [volume, audioRef, muteVolume]);
```

Save and test the volume functionality. It should work as expected!

## Triggering the `onEnded` event <a name="triggering-onended-event">

When the current track reaches the end, we can trigger the `onended` event on the `<audio>` element and act. For this project, we will tell the audio player to start the next track on the playlist. 

Since we already have the functionality in the `handleNext` handler, let’s move the handler from the `components/Controls.js` file into the `AudioPlayer` parent component so that we can also have access to it in the `DisplayTrack` component. 

Then pass it down to the `<DisplayTrack />` and `<Controls />` components:

```javascript
return (
  <div className="audio-player">
    <div className="inner">
      <DisplayTrack
        {...{
          // ...
          handleNext,
        }}
      />
      <Controls
        {...{
          // ...
          handleNext,
        }}
      />
      {/* ... */}
    </div>
  </div>
);
```

In the `DisplayTrack` component, access the `handleNext` handler as a prop and assign it to the `onEnded` event on the `<audio />` element:

```javascript
<audio
  // ...
  onEnded={handleNext}
/>
```

Save the file and go into the `Controls` component to access the `handleNext` handler as a prop. Remember, we’ve attached it to the next button icon in the file. 

Save all files and test your project. It should work as intended!

## Conclusion

I’m glad you are here! Creating audio players can be a tedious task, especially if we aim to include major features. 

In this guide, we learned how to create a React audio player that offers necessary features, including the play, pause, volume, next, previous, and progress controls. We also ensured the audio player looks consistent across browsers. 

I hope you enjoyed reading this guide. Let me know your thoughts in the comment section, and remember to share the guide around the web if you found it useful. 

See the [final source code](https://github.com/Ibaslogic/react-audio-player) and [the live project](https://github.com/Ibaslogic/react-audio-player) for reference.

---
## Cut through the noise of traditional React error reporting with LogRocket

[LogRocket](https://lp.logrocket.com/blg/react-signup-issue-free) is a React analytics solution that shields you from the hundreds of false-positive errors alerts to just a few truly important items. LogRocket tells you the most impactful bugs and UX issues actually impacting users in your React applications.

[![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/react-signup-general)

[LogRocket](https://lp.logrocket.com/blg/react-signup-issue-free) automatically aggregates client side errors, React error boundaries, Redux state, slow component load times, JS exceptions, frontend performance metrics, and user interactions. Then LogRocket uses machine learning to notify you of the most impactful problems affecting the most users and provides the context you need to fix it.

Focus on the React bugs that matter — [try LogRocket today](https://lp.logrocket.com/blg/react-signup-issue-free).