Build a custom audio player with accessibility
Tags: a11y, react, javascript, audio
Published: 2019-06-28 08:15:29

---
title: Build a custom audio player with accessibility
published: true
tags: accessibility,react,javascript,audio
cover_image: https://thepracticaldev.s3.amazonaws.com/i/8sca9ev2t877jyuoqgr3.jpg
canonical_url: https://blog.atrera.com/javascript/accessibility/2019/06/28/create-a-custom-audio-player-with-accessibility.html
---

> This article was originally posted in my [personal blog](https://blog.atrera.com/javascript/accessibility/2019/06/28/create-a-custom-audio-player-with-accessibility.html).

This is not my first time to create [a custom audio player](https://codepen.io/snowleo208/pen/zJMqog). 

In order to cater for my lovely designer's needs, I need to create a custom audio player. After learnt [HTML5 Audio attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio) and viewed more tutorials of making custom audio player, most of the tutorial did not mention accessibility.

This time I use **React** for audio player, but you can always view my last custom audio player for vanilla JavaScript version (although did not care much about accessibility at that moment).

I am not an expert of accessibility at all, feel free to let me know your thoughts!

---

## The markup of player

First, the outer container of the audio player, it should have `role="region"` and `aria-label="Audio Player"`. Role tells screen reader this `div` represents something, and from `aria-label`, the screen reader knows it is an audio player.

### Outer container

```javascript
<div className="c-audio" aria-label="Audio Player" role="region">
  // ...
</div>
```

### Play button

The following is the play button inside:

```javascript
<button
  title={!isPlay || isPlay === null ? 'Play' : 'Pause'}
  className={
    !isPlay || isPlay === null
      ? 'c-audio u-btn l-play l-play__play'
      : 'c-audio u-btn l-play l-play__pause'
  }
  aria-controls="audio1"
  onClick={this.controlAudio}
  aria-label={!isPlay || isPlay === null ? 'Play' : 'Pause'}
/>
```

The `aria-controls` links to the id of the `audio` tag at the bottom (e.g. `<audio id="audio1" ... >`) and `aria-label` changes while playing or pausing.

### Slider

For the audio control, actually I want to use range (e.g. `<input type="range" ...>`), however, it is hard to maintain same styles in all browsers, so I decided to use `div` with `svg`, plus aria labels. Also, I used `tabIndex="0"` here for keyboard to focus into this element.

In this slider, user can:

- use mouse or keyboard to change current time of audio
- can focus the slider
- can use mouse to change position
- can use left or right key in keyboard to change position

```javascript
<div
className="c-audio__slider"
onKeyDown={this.onKeyDown}
onClick={this.onClick}
tabIndex="0"
aria-valuetext="seek audio bar"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow={Math.round(percentage)}
role="slider"
ref={this.audioSeekBar}
>
```

It needs lots of work to reinvent the slider, but it is worth it. After these implementations, you can create slider with different styles, also with accessibility! View here for example from [WAI-ARIA Authoring Practices](https://www.w3.org/TR/wai-aria-practices/examples/slider/slider-1.html).

### Manipulate the slider

How to change the percentage of slider when it detects click or key down? We can use `onClick` and `onKeyDown` function. For the click function, it calculates the percentage of click position. (Note: `seekBar.getBoundingClientRect().left` is for IE11, as it doesn’t support `x/y` values)

```javascript
onClick(e) {
    const seekBar = this.audioSeekBar.current;
    const audio = this.audioFile.current;

    const pos =
    (e.pageX -
        (seekBar.getBoundingClientRect().x ||
        seekBar.getBoundingClientRect().left)) /
        seekBar.getClientRects()[0].width;

        this.setState({
            percentage: pos * 100
        });

        audio.currentTime = audio.duration * pos;
}
```

For keyboard version, it add or decrease percentages based on different keys.

Keybindings for slider:

- top: to 100 (max)
- bottom: to 0 (min)
- left: -1 step
- right: +1 step
- top: +10 steps
- bottom: -10 steps

```javascript
onKeyDown(e) {
    // when user focus in audio slider and 
    // clicks keys inside key list, will change current time of audio
    const audio = this.audioFile.current;
    const isLeft = 37;
    const isRight = 39;
    const isTop = 38;
    const isBottom = 40;
    const isHome = 36;
    const isEnd = 35;
    const keyList = [isLeft,isRight,isTop,isBottom,isHome,isEnd];

    if (keyList.indexOf(e.keyCode) >= 0) {
        let percentage;
        switch(e.keyCode) {
            case isLeft:
            percentage = parseFloat(this.state.percentage) - 1
            break;
            case isRight:
            percentage = parseFloat(this.state.percentage) + 1
            break;
            case isTop:
            percentage = parseFloat(this.state.percentage) + 10
            break;
            case isBottom:
            percentage = parseFloat(this.state.percentage) - 10
            break;
            case isHome:
            percentage = 0
            break;
            case isEnd:
            percentage = 99.9 // 100 would trigger onEnd, so only 99.9
            break;
            default:
            break;
        }

        // add boundary for percentage, cannot be bigger than 100 or smaller than zero
        if(percentage > 100) {
            percentage = 100
        } else if(percentage < 0) {
            percentage = 0
        }

        this.setState({
            percentage
        });

        audio.currentTime = audio.duration * (percentage / 100);
    }
}
```

### Audio Tag

The main thing here is the audio tag. From the audio tag, we need to use `onTimeUpdate` and `onEnded` to control the slider. When the audio is running, it calls the function of `onTimeUpdate` and update the slider.

When the audio ends, it changes the current time of audio to zero and change slider’s percentage to zero too. For the `<track kind="captions" />`, it is for audio or video which has subtitles, we don't have one here, so skip it right now.

```javascript
<audio
  className="c-audio__sound"
  id="audio1"
  src={path}
  onTimeUpdate={this.getCurrDuration}
  onEnded={() => {
    this.audioFile.current.currentTime = 0;
    this.setState({
      isPlay: false,
      currentTime: 0,
      percentage: 0
    });
  }}
  ref={this.audioFile}
>
  <track kind="captions" />
</audio>
```

### Focus Styles

Also, don’t forget to create custom focus styles for play button and slider!

```javascript
.l-play:focus {
  outline: none;
  box-shadow: 1px 1px 1px 0px rgba(25, 25, 25, 0.2);
}
```

### Result

View my result in the following or [click here to view on Codepen](https://codepen.io/snowleo208/pen/Zdvzym)!

{% codepen https://codepen.io/snowleo208/pen/Zdvzym default-tab=js,result %}

Welcome to drop me a line or let me know your thoughts! :)

---

## Read More

- [A Sliding Nightmare: Understanding the Range Input](https://css-tricks.com/sliding-nightmare-understanding-range-input/)
- [Dev.Opera - A More Accessible HTML5 video Player](https://dev.opera.com/articles/more-accessible-html5-video-player/)
- [WAI-ARIA Authoring Practices](https://www.w3.org/TR/wai-aria-practices/examples/slider/slider-1.html)