<section id="index" class="wrapper"></section>
<section class="instruction">
  <h1 class="u-title">Custom Audio Player</h1>

  <p>User can: </p>
  <ol>
    <li>Play the audio</li>
    <li>Pause the audio</li>
    <li>Change audio position using the slider (with mouse / keyboard)</li>
  </ol>

  <p>In the slider, user can:</p>
  <ul>
    <li>Use mouse or keyboard to change current time of audio</li>
    <li>Can focus the slider</li>
    <li>Can use mouse to change position</li>
    <li>Can use left or right key in keyboard to change position</li>
  </ul>

  <p>Keybindings for slider (after focused):</p>
  <li>Top: to 100 (max)</li>
  <li>Bottom: to 0 (min)</li>
  <li>Left: -1 step</li>
  <li>Right: +1 step</li>
  <li>Top: +10 steps</li>
  <li>Bottom: -10 steps</li>
</section>
$theme: #ccc;

//media query
$mobile: "only screen and (min-width : 576px)";
$laptop: "only screen and (min-width : 992px)";

/*------------------------------------*\
  #Base
\*------------------------------------*/

body {
  font-family: "Lato", "Calibri", sans-serif;
  width: 100%;
  height: auto;
  min-height: 100%;
  background: white;
  line-height: 1.48;
  margin: 0;
  padding: 1em;
  overflow: auto;
}

html {
  width: 100%;
  margin: 0;
  padding: 0;
  overflow: auto;
}

* {
  box-sizing: border-box;
}

header,
section {
  margin-bottom: 4em;

  @media #{$mobile} {
    margin-bottom: 2em;
  }
}

/*------------------------------------*\
  #Audio Player
\*------------------------------------*/

.u-bg {
  position: relative;
  display: block;
  background-image: none !important;
  width: auto;
  height: auto;
  margin: 0;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.c-audio {
  display: flex;
  align-items: center;
  width: 100%;
  height: auto;
  background: $theme;
  border-radius: 0.25em;
  padding: 0 1em 0 0;

  .l-play {
    position: relative;
    justify-content: center;
    appearance: none;
    width: 12%;
    min-width: 40px;
    height: 40px;
    border: 0;
    background: $theme;
    color: white;
    text-align: center;
    cursor: pointer;
    border-radius: 0;
    padding: 0;

    &:focus {
      outline: none;
      box-shadow: 1px 1px 1px 0px rgba(25,25,25,0.2);
    }

    &__play,
    &__pause {
      background-size: contain;
      background-position: center;
      background-repeat: no-repeat;
    }

    &__play {
      background-image: url('data:image/svg+xml;utf8,<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"><g id="playbtn"><g transform="translate(-670.000000, -353.000000)"><path id="play_btn" style="fill:%23FFFFFF;" d="M740.3,403.5l-31.7-20.8v41.6L740.3,403.5z"/></g></g></svg>');
    }

    &__pause {
      background-image: url('data:image/svg+xml;utf8,<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"><g id="pause"><g transform="translate(-968.000000, -353.000000)"><path id="pause_btn" style="fill:%23FFFFFF;" d="M1003.6,382.7v41.6h7.9v-41.6H1003.6z M1025.4,382.7v41.6h7.9v-41.6H1025.4z"/></g></g></svg>');
    }
  }

  &__slider {
    position: relative;
    display: flex;
    align-items: center;
    width: 88%;
    height: 40px;
    overflow: hidden;
    cursor: pointer;

    &:focus {
      outline: none;
      box-shadow: 1px 1px 1px 0px rgba(25,25,25,0.2);
    }
  }

  &__length {
    position: relative;
    width: 100%;
    height: 0.4em;
    border-radius: 0.5em;
    background: rgba(19, 19, 19, 0.3);
    overflow: hidden;
  }

  &__bar {
    position: absolute;
    display: block;
    left: 0;
    top: 0;
    width: 100%;
    height: 0.4em;
    border-radius: 0.5em;
    border: 0;
    appearance: none;
    transition: none;
    overflow: hidden;
    fill: white;
    stroke: none;

    &:focus {
      outline: 1;
      box-shadow: 13px 12px 12px -4px rgba(19, 19, 19, 0.5);
    }
  }
}
View Compiled
class Player extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isPlay: null,
      currDuration: 0,
      percentage: 0,
      seekBar: 0
    };
    this.audioFile = React.createRef();
    this.audioSeekBar = React.createRef();
    this.getCurrDuration = this.getCurrDuration.bind(this);
    this.controlAudio = this.controlAudio.bind(this);
    this.onClick = this.onClick.bind(this);
    this.onKeyDown = this.onKeyDown.bind(this);
  }

  getCurrDuration() {
    const audio = this.audioFile.current;
    const percentage = ((audio.currentTime / audio.duration) * 100).toFixed(2);

    // get current percentage and duration
    this.setState({
      currDuration: audio.currentTime,
      percentage
    });
  }

  controlAudio() {
    const audio = this.audioFile.current;
    if (audio.duration > 0 && !audio.paused) {
      // pause audio and stop counting
      audio.pause();
      this.setState({
        isPlay: false
      });
    } else {
      // play audio
      audio.play();
      this.setState({
        isPlay: true
      });
    }
  }

  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);
    }
  }

  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;
  }

  render() {
    const { percentage, isPlay } = this.state;
    const { path, image } = this.props;
    return (
      <React.Fragment>
        <div className="u-bg" style={{ backgroundImage: `url("${image}")` }}>
          <div className="c-audio" aria-label="Audio Player" role="region">
            <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'}
            />
            <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}
            >
              <div className="c-audio__length">
                <svg
                  className="c-audio__bar"
                  viewBox="0 0 100% 6"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <rect x="0" width={percentage + '%'} height="6" rx="3" />
                </svg>
              </div>
            </div>
          </div>
        </div>

        <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>
      </React.Fragment>
    );
  }
}

Player.propTypes = {
  path: PropTypes.string,
  image: PropTypes.string
};

class App extends React.Component {
  render() {
    return (
      <Player
        path="//cdn.atrera.com/audio/Marcel_Pequel_-_01_-_One.mp3"
        image="//cdn.atrera.com/images/cover_yz2mak.jpg"
      />
    );
  }
}


ReactDOM.render(<App />, document.getElementById("index"));
View Compiled

External CSS

  1. https://fonts.googleapis.com/css?family=Lato&amp;display=swap

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.7.2/prop-types.min.js