Building a Custom AudioPlayer Component in JavaScript
Creating a custom AudioPlayer component in JavaScript gives you full control over playback behavior, UI, accessibility, and integration with frameworks. This guide walks through building a lightweight, extensible AudioPlayer using plain JavaScript, with notes on accessibility, responsiveness, and optional framework integration.
What you’ll build
A reusable AudioPlayer that supports:
- Play / pause
- Seek bar with buffered progress
- Volume control and mute
- Track duration / current time display
- Keyboard accessibility
- Events for external integration (track end, error, play, pause)
File structure
- index.html
- styles.css
- audio-player.js
HTML markup
Use semantic, minimal markup so the component can be instantiated many times.
html
<div class=“audio-player” data-src=“assets/audio/sample.mp3” role=“group” aria-label=“Audio player”> <button class=“ap-play” aria-label=“Play”>►</button> <div class=“ap-timeline” aria-label=“Seek bar” role=“slider” tabindex=“0” aria-valuemin=“0” aria-valuemax=“100” aria-valuenow=“0”> <div class=“ap-buffer”></div> <div class=“ap-progress”></div> </div> <div class=“ap-time”> <span class=“ap-current”>0:00</span> / <span class=“ap-duration”>0:00</span> </div> <button class=“ap-mute” aria-label=“Mute”>🔊</button> <input class=“ap-volume” type=“range” min=“0” max=“1” step=“0.01” value=“1” aria-label=“Volume”> </div>
CSS (basic)
Keep styles modular so they can be themed.
css
.audio-player { display:flex; align-items:center; gap:8px; font-family:system-ui; } .ap-timeline { position:relative; width:240px; height:8px; background:#eee; cursor:pointer; border-radius:4px; } .ap-buffer, .ap-progress { position:absolute; left:0; top:0; height:100%; border-radius:4px; } .ap-buffer { background:#ddd; width:0%; } .ap-progress { background:#2196f3; width:0%; } .ap-play, .ap-mute { background:none; border:0; cursor:pointer; font-size:16px; } .ap-time { font-size:12px; color:#333; min-width:68px; text-align:center; } .ap-volume { width:80px; }
JavaScript: core component
Create a class that encapsulates behaviors and exposes events.
javascript
class AudioPlayer { constructor(root) { this.root = root; this.src = root.dataset.src; this.audio = new Audio(this.src); this.playBtn = root.querySelector(’.ap-play’); this.timeline = root.querySelector(’.ap-timeline’); this.bufferEl = root.querySelector(’.ap-buffer’); this.progressEl = root.querySelector(’.ap-progress’); this.currentEl = root.querySelector(’.ap-current’); this.durationEl = root.querySelector(’.ap-duration’); this.muteBtn = root.querySelector(’.ap-mute’); this.volumeInput = root.querySelector(’.ap-volume’); this.init(); } init() { this.bindUI(); this.audio.preload = ‘metadata’; this.audio.addEventListener(‘loadedmetadata’, () => { this.durationEl.textContent = this.formatTime(this.audio.duration); this.timeline.setAttribute(‘aria-valuemax’, Math.floor(this.audio.duration)); }); this.audio.addEventListener(‘timeupdate’, () => this.updateProgress()); this.audio.addEventListener(‘progress’, () => this.updateBuffer()); this.audio.addEventListener(‘ended’, () => this.onEnded()); this.audio.addEventListener(‘play’, () => this.playBtn.textContent = ‘❚❚’); this.audio.addEventListener(‘pause’, () => this.playBtn.textContent = ‘►’); this.audio.addEventListener(‘volumechange’, () => this.onVolumeChange()); } bindUI() { this.playBtn.addEventListener(‘click’, () => this.togglePlay()); this.muteBtn.addEventListener(‘click’, () => { this.audio.muted = !this.audio.muted; this.muteBtn.textContent = this.audio.muted ? ‘🔇’ : ‘🔊’; }); this.volumeInput.addEventListener(‘input’, (e) => { this.audio.volume = parseFloat(e.target.value); }); // Seek via click this.timeline.addEventListener(‘click’, (e) => { const rect = this.timeline.getBoundingClientRect(); const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); this.audio.currentTime = pct this.audio.duration; this.updateProgress(); }); // Keyboard seeking this.timeline.addEventListener(‘keydown’, (e) => { if (!this.audio.duration) return; const step = Math.max(1, Math.floor(this.audio.duration / 20)); if (e.key === ‘ArrowRight’) this.audio.currentTime = Math.min(this.audio.duration, this.audio.currentTime + step); if (e.key === ‘ArrowLeft’) this.audio.currentTime = Math.max(0, this.audio.currentTime - step); this.updateProgress(); }); } togglePlay() { if (this.audio.paused) this.audio.play(); else this.audio.pause(); } updateProgress() { if (!this.audio.duration) return; const pct = (this.audio.currentTime / this.audio.duration) 100; this.progressEl.style.width = pct + ’%’; this.currentEl.textContent = this.formatTime(this.audio.currentTime); this.timeline.setAttribute(‘aria-valuenow’, Math.floor(this.audio.currentTime)); } updateBuffer() { try { const ranges = this.audio.buffered; if (ranges.length) { const end = ranges.end(ranges.length - 1); const pct = (end / this.audio.duration) 100; this.bufferEl.style.width = pct + ’%’; } } catch (e) { / ignore */ } } onEnded() { this.root.dispatchEvent(new CustomEvent(‘ap-ended’, { bubbles: true })); } onVolumeChange() { this.volumeInput.value = this.audio.volume; this.muteBtn.textContent = this.audio.muted ? ‘🔇’ : ‘🔊’; } formatTime(sec = 0) { const s = Math.floor(sec % 60).toString().padStart(2,‘0’); const m = Math.floor(sec / 60); return</span><span class="token template-string interpolation interpolation-punctuation" style="color: rgb(57, 58, 52);">${</span><span class="token template-string interpolation">m</span><span class="token template-string interpolation interpolation-punctuation" style="color: rgb(57, 58, 52);">}</span><span class="token template-string" style="color: rgb(163, 21, 21);">:</span><span class="token template-string interpolation interpolation-punctuation" style="color: rgb(57, 58, 52);">${</span><span class="token template-string interpolation">s</span><span class="token template-string interpolation interpolation-punctuation" style="color: rgb(57, 58, 52);">}</span><span class="token template-string template-punctuation" style="color: rgb(163, 21, 21);">; } } // Auto-init all players on the page document.addEventListener(‘DOMContentLoaded’, () => { document.querySelectorAll(’.audio-player’).forEach(el => new AudioPlayer(el)); });
Accessibility notes
- Use role=“slider” and aria-valuenow/aria-valuemin/aria-valuemax on the timeline.
- Ensure buttons have aria-labels.
- Keyboard support: left/right arrows for seeking; space/enter could toggle play if the container is focusable.
- Announce track changes via ARIA live regions if needed.
Extensibility ideas
- Add playlist support and next/previous controls.
- Add waveform visualization using Web Audio API + Canvas.
- Persist volume and playback position in localStorage.
- Expose a small API for external control: play(), pause(), load(src), seek(t).
Integration with React/Vue
- Wrap the markup and logic into a component; use refs for the audio element and lifecycle hooks to manage events.
- Keep the core class (AudioPlayer) and call it from frameworks to avoid rewriting audio logic.
Testing & performance tips
- Test on mobile browsers where autoplay and muted policies differ.
- Use small audio files or range requests for long tracks.
- Debounce expensive UI updates (e.g., heavy waveform draws) to animation frames.
This component provides a solid, accessible foundation you can customize for design, features, and framework integration.
Leave a Reply