Build a custom audio player with accessibility

2019-06-28 · 20 min read

This is not my first time to create a custom audio player.

In order to cater for my lovely designer's needs, I need to create a custom audio player. After learnt HTML5 Audio attributes 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

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

Play button

The following is the play button inside:

<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
<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.

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)

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
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 have none here, so skip it.

<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!

.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!

See the Pen A11y Audio Player by Yuki (@snowleo208) on CodePen.

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


Read More



Written by Yuki Cheung

Hey there, I am Yuki! I'm a front-end software engineer who's passionate about both coding and gaming.

When I'm not crafting code, you'll often find me immersed in the worlds of Fire Emblem and The Legend of Zelda—those series are my absolute favorites! Lately, I've been delving into the fascinating realm of AI-related topics too.