Core concepts
The instance and its options
Every wavesurfer.js integration starts with a single call:
import WaveSurfer from 'wavesurfer.js'
const ws = WaveSurfer.create(options)
WaveSurfer.create() accepts an options object of type WaveSurferOptions. Only container is required; everything else has a default.
Most-used options
| Option | Type | Description |
|---|---|---|
container |
HTMLElement | string |
Required. The element (or CSS selector) where the waveform is rendered. |
url |
string |
Audio file URL to load on creation. |
waveColor |
string | string[] | CanvasGradient |
Color of the unplayed part of the waveform. Default: '#999'. |
progressColor |
string | string[] | CanvasGradient |
Color of the played-through portion. Default: '#555'. |
cursorColor |
string |
Color of the playback cursor line. |
cursorWidth |
number |
Width of the cursor in pixels. Default: 1. |
height |
number | 'auto' |
Height of the waveform in pixels, or 'auto' to fill the container’s height. |
barWidth |
number |
Renders a bar-style waveform (▁▂▇▃▅▂) with bars of this pixel width. |
barGap |
number |
Spacing between bars in pixels. |
barRadius |
number |
Corner radius of bars in pixels. |
minPxPerSec |
number |
Minimum pixels per second of audio (the zoom level). Default: 0. |
normalize |
boolean |
Stretch amplitude to use the full waveform height. |
interact |
boolean |
Enable click-to-seek on the waveform. Default: true. |
dragToSeek |
boolean | { debounceTime: number } |
Allow dragging the cursor to seek. Pass true (200 ms debounce) or an object to customise. Default: false. |
autoScroll |
boolean |
Scroll the container to keep the playhead in view during playback. Default: true. |
autoCenter |
boolean |
Keep the cursor centred while autoScroll is active. Default: true. |
hideScrollbar |
boolean |
Hide the horizontal scrollbar when the waveform overflows. |
mediaControls |
boolean |
Show the browser’s native audio controls below the waveform. |
audioRate |
number |
Playback speed multiplier (e.g. 0.5 for half speed). |
peaks |
Array<Float32Array | number[]> |
Pre-computed peak data — skips client-side decoding. See Pre-decoded peaks. |
duration |
number |
Audio duration in seconds; required when providing peaks without a URL. |
media |
HTMLMediaElement |
Supply your own <audio> or <video> element instead of letting wavesurfer create one. |
backend |
'MediaElement' | 'WebAudio' |
Playback engine. Default: 'MediaElement'. See the next section. |
Changing options after creation
Call ws.setOptions(partialOptions) at any time to update one or more options. The waveform re-renders automatically:
ws.setOptions({ waveColor: '#1a73e8', barWidth: 3 })
How audio is played: MediaElement vs WebAudio
wavesurfer.js supports two playback backends, selected via the backend option.
MediaElement (default)
The default backend uses a standard <audio> element. The browser streams and decodes the file progressively, so playback can begin before the file has fully downloaded. This is ideal for:
- Long files or podcast audio where you do not need the full file in memory
- Streaming sources (HTTP Live Streaming, etc.)
- Scenarios where low initial memory use matters
wavesurfer still fetches and decodes a copy of the audio separately (at the lower sample rate set by sampleRate) to draw the waveform. The media element and the decoded waveform data are kept in sync but are separate objects.
WebAudio backend
Pass backend: 'WebAudio' to route playback through a Web Audio AudioContext and AudioBufferSourceNode:
const ws = WaveSurfer.create({
container: '#waveform',
url: '/audio.mp3',
backend: 'WebAudio',
})
The entire file is fetched and decoded into an AudioBuffer before playback can start. Trade-offs:
| MediaElement | WebAudio | |
|---|---|---|
| Starts playing | Progressively (fast) | Only after full decode |
| Memory use | Low | High — entire file decoded |
| Audio effects / filters | Limited | Full Web Audio graph via GainNode |
| Streaming sources | Yes | No |
With backend: 'WebAudio', large files are fetched twice — once by the media element path for waveform decoding, and again by the AudioContext for playback. For files over a few minutes long this can cause noticeable memory pressure. Prefer the default MediaElement backend and use Pre-decoded peaks to avoid the double decode.
Lifecycle
Understanding the sequence of events helps you wire up your UI correctly.
Creation to ready
WaveSurfer.create(options)
→ (async) load starts
→ 'decode' fired when audio data has been decoded and the waveform is drawn
→ 'ready' fired when the audio is decoded *and* can be played
In code:
const ws = WaveSurfer.create({ container: '#waveform', url: '/audio.mp3' })
ws.on('decode', (duration) => {
console.log('Waveform drawn, duration:', duration)
})
ws.on('ready', (duration) => {
console.log('Ready to play, duration:', duration)
ws.play()
})
Both decode and ready receive the audio duration in seconds as their first argument. In most cases ready is the right place to start playback or enable your UI controls.
When you supply pre-decoded peaks and duration (and no url), wavesurfer skips network loading and fires decode / ready immediately after the waveform is drawn.
Teardown
Always call ws.destroy() when the component or page that owns the waveform is removed. It stops playback, removes all DOM nodes, cancels any in-flight network requests, and unregisters all event listeners and plugins.
// React example
useEffect(() => {
const ws = WaveSurfer.create({ container: ref.current, url })
return () => ws.destroy()
}, [url])
Failing to destroy leaves orphaned DOM nodes and event listeners, which can degrade performance in single-page applications over time. See Performance for further guidance.
Sizing the waveform
The waveform always fills the full width of its container element. Do not set a fixed width on the container if you want the waveform to be responsive — let it follow the parent’s width naturally.
Height
Control the height with the height option:
// Fixed height in pixels
WaveSurfer.create({ container: '#waveform', height: 80 })
// Fill the container's height
WaveSurfer.create({ container: '#waveform', height: 'auto' })
With a numeric value, the canvas is exactly that many pixels tall regardless of the container. With 'auto', wavesurfer reads the container’s clientHeight and uses that — useful when the container is already sized by your CSS.
Common sizing pitfalls:
-
height: 'auto'with no parent height — if the container has no explicit height (e.g. it isheight: 0or relies on content to expand it), wavesurfer will render a zero-height canvas. Always give the container a real height via CSS before using'auto'. -
Avoid
width: fit-contenton the container — the waveform wrapper expands to match the zoom level, sofit-contentcauses a feedback loop that inflates the container width indefinitely. Usewidth: 100%(or any fixed/percentage value) instead. -
High
devicePixelRatio— on retina displays the canvas pixel dimensions are multiplied by the device pixel ratio for sharpness, which increases GPU memory use. This is automatic and generally not a concern, but be aware that the canvas’s internal width in pixels is larger than the CSS width.
Responsive resizing
wavesurfer attaches a ResizeObserver to its scroll container and automatically re-renders the waveform when the container changes size. No manual intervention is needed — just make sure the container is in the DOM and has a measurable size when WaveSurfer.create() is called.