Zero-config audio waveforms for the web.
Add a data- attribute to a <div> — get a real, interactive waveform player. No build step, no dependencies, ~10KB.
No build tools. No initialization. Drop in two files and add a <div>.
<link rel="stylesheet" href="https://unpkg.com/@arraypress/waveform-player/dist/waveform-player.css">
<script src="https://unpkg.com/@arraypress/waveform-player/dist/waveform-player.min.js"></script>
<div data-waveform-player data-url="track.mp3" data-title="My Song"></div>It auto-initializes every [data-waveform-player] on the page when the DOM is ready. That's it.
npm install @arraypress/waveform-playerimport WaveformPlayer from '@arraypress/waveform-player';
import '@arraypress/waveform-player/styles.css';
const player = new WaveformPlayer('#player', { url: 'track.mp3' });Ships ESM + CommonJS + a standalone IIFE for <script> tags, with bundled TypeScript definitions.
| WaveformPlayer | WaveSurfer.js | Amplitude.js | |
|---|---|---|---|
| Size (gzipped) | ~10KB | 40KB+ | 35KB+ |
| Zero-config (HTML) | ✓ | — | — |
| Dependencies | None | None | None |
| Real waveforms | ✓ | ✓ | — |
| TypeScript types | ✓ | ✓ | — |
| Keyboard + ARIA | ✓ | partial | — |
| Media Session API | ✓ | — | — |
- Zero-config — works from plain HTML via
data-attributes; no JS required. - 6 visual styles — bars, mirror, line, blocks, dots, seekbar — plus rounded caps and gradient fills.
- Real audio analysis — decodes peaks with the Web Audio API, or uses pre-generated data.
- Accessible — the waveform is a keyboard-operable ARIA slider out of the box.
- Framework-ready — first-party React and Astro wrappers; works anywhere otherwise.
- Batteries included — chapter markers, BPM detection, Media Session, speed control, auto theme detection.
- Tiny — ~10KB gzipped, zero dependencies, TypeScript types bundled.
Set with data-style (or the longer data-waveform-style), or the style / waveformStyle option.
| Style | Look |
|---|---|
mirror |
Symmetrical, SoundCloud-style (default) |
bars |
Classic bottom-anchored bars |
line |
Smooth oscilloscope line |
blocks |
Segmented LED meter |
dots |
Circular points |
seekbar |
Minimal progress bar, no peaks |
Modern caps & gradients (bundle-neutral):
new WaveformPlayer('#player', {
url: 'track.mp3',
waveformStyle: 'mirror',
barRadius: 3, // rounded bar caps
waveformColor: ['#fafafa', '#71717a'], // vertical gradient (array of stops)
progressColor: ['#ffffff', '#a1a1aa'],
});In zero-config markup, pass a gradient as a JSON array: data-waveform-color='["#fafafa","#71717a"]'.
<div data-waveform-player
data-url="track.mp3"
data-title="My Song"
data-subtitle="Artist Name"
data-waveform-style="mirror"
data-bar-radius="3"
data-show-playback-speed="true"
data-markers='[{"time": 30, "label": "Chorus"}]'>
</div><div data-waveform-player
data-url="track.mp3"
data-show-controls="false"
data-show-info="false">
</div>import WaveformPlayer from '@arraypress/waveform-player';
const player = new WaveformPlayer('#player', {
url: 'track.mp3',
waveformStyle: 'mirror',
height: 80,
markers: [
{ time: 30, label: 'Verse', color: '#4ade80' },
{ time: 60, label: 'Chorus', color: '#f59e0b' },
],
});| Option | Type | Default | Description |
|---|---|---|---|
url |
string |
'' |
Audio file URL (alias: src / data-src) |
waveformStyle |
string |
'mirror' |
bars · mirror · line · blocks · dots · seekbar |
height |
number |
60 |
Waveform height (px) |
barWidth |
number |
style-based | Bar width (px) |
barSpacing |
number |
style-based | Gap between bars (px) |
barRadius |
number |
0 |
Rounded bar-cap radius (px) — bars/mirror |
waveformColor |
string | string[] |
preset | Unplayed colour; array = vertical gradient |
progressColor |
string | string[] |
preset | Played colour; array = vertical gradient |
colorPreset |
'dark' | 'light' | null |
null |
Force a theme, or null to auto-detect |
samples |
number |
200 |
Peak samples to extract |
waveform |
number[] | string |
null |
Pre-generated peaks (array, .json URL, or JSON) |
audioMode |
'self' | 'external' |
'self' |
Own the <audio>, or delegate (see below) |
markers |
WaveformMarker[] |
[] |
Chapter markers |
showControls |
boolean |
true |
Show the play/pause button |
showInfo |
boolean |
true |
Show title/subtitle/time |
showPlaybackSpeed |
boolean |
false |
Show the speed menu |
showBPM |
boolean |
false |
Detect + display BPM |
autoplay |
boolean |
false |
Play on load |
enableMediaSession |
boolean |
true |
System media controls (self mode) |
accessibleSeek |
boolean |
true |
Expose the waveform as a keyboard ARIA slider |
seekLabel |
string |
null |
Accessible name for the slider (falls back to title) |
Full option types ship in index.d.ts.
By default the waveform is a keyboard-operable ARIA slider: .waveform-container gets role="slider", joins the tab order, and reports aria-valuemin/max/now plus a readable aria-valuetext (e.g. "0:30 of 2:00"). When focused:
| Key | Action |
|---|---|
| ← / ↓ · → / ↑ | Seek ∓5s |
| Page Down / Page Up | Seek ∓10s |
| Home / End | Start / end |
Works in both audio modes, respects prefers-reduced-motion, and announces load errors to screen readers. Opt out with accessibleSeek: false; localize with seekLabel.
player.play(); // returns the <audio>.play() promise in self mode
player.pause();
player.togglePlay();
player.seekTo(30); // seconds
player.seekToPercent(0.5);
player.setVolume(0.8);
player.setPlaybackRate(1.5);
await player.loadTrack('next.mp3', 'Title', 'Artist', { artwork: 'cover.jpg' });
player.destroy(); // removes all listeners + DOM
// Statics
WaveformPlayer.getInstance('id');
WaveformPlayer.getAllInstances();
WaveformPlayer.destroyAll();
const peaks = await WaveformPlayer.generateWaveformData('track.mp3');
const jsonUrl = WaveformPlayer.getPeaksUrl('track.mp3'); // -> 'track.json'Lifecycle callbacks via options, or DOM events on the container (all bubble, e.detail is typed):
new WaveformPlayer('#player', {
url: 'track.mp3',
onPlay: (player) => {},
onPause: (player) => {},
onEnd: (player) => {},
onTimeUpdate: (currentTime, duration, player) => {},
});
el.addEventListener('waveformplayer:timeupdate', (e) => {
const { currentTime, duration, progress } = e.detail;
});Set audioMode: 'external' and the player becomes a visualization-only surface: play/pause/seek dispatch cancelable events, and you drive the canvas back with setPlayingState() / setProgress(). This is how @arraypress/waveform-bar turns many inline players into one persistent bar.
const player = new WaveformPlayer(el, { audioMode: 'external' });
el.addEventListener('waveformplayer:request-play', (e) => {
audio.src = e.detail.url; // e.detail = { url, title, subtitle, artist, artwork, id, player }
audio.play();
});
audio.addEventListener('timeupdate', () => player.setProgress(audio.currentTime, audio.duration));
audio.addEventListener('play', () => player.setPlayingState(true));
audio.addEventListener('pause', () => player.setPlayingState(false));| Event | Detail |
|---|---|
waveformplayer:request-play |
{ url, title, subtitle, artist, artwork, id, player } |
waveformplayer:request-pause |
Same shape |
waveformplayer:request-seek |
Same shape + { percent: 0..1 } |
| Environment | Package |
|---|---|
| React | @arraypress/waveform-player-react |
| Astro | @arraypress/waveform-player-astro |
| Vanilla / anything else | this package — new WaveformPlayer(el, opts) and player.destroy() on teardown |
- WaveformBar — persistent bottom-bar player with queue, favorites, and cross-page session.
- WaveformPlaylist — zero-JS playlists and chapters.
- WaveformGen — pre-generate peak JSON at build time.
- WaveformTracker — audio engagement analytics.
Skip client-side decoding by storing peaks next to each file (track.mp3 ↔ track.json):
new WaveformPlayer('#player', {
url: track.audioUrl,
waveform: WaveformPlayer.getPeaksUrl(track.audioUrl),
});getPeaksUrl() swaps a known audio extension (mp3, wav, ogg, flac, m4a, aac) for .json, preserving query/hash, and returns undefined for anything else (so the player falls back to live decoding).
Chrome/Edge 90+, Firefox 88+, Safari 14+, and modern mobile browsers. Rounded bar caps use roundRect where available (Safari 16+) and fall back to square bars elsewhere.
npm install
npm run dev # watch build
npm test # vitest
npm run build # all bundles
npm run size # gzipped sizeMIT © ArrayPress · created by David Sherlock