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 is height: 0 or 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-content on the container — the waveform wrapper expands to match the zoom level, so fit-content causes a feedback loop that inflates the container width indefinitely. Use width: 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.