Skip to content

wavesurfer libraries — comparison

wavesurfer.js does the real work (canvas rendering + Web Audio). The question is the wrapper: how do you drop a waveform into your framework with idiomatic props, events, two-way state, and an imperative handle? Today that answer is uneven across ecosystems — which is the gap @rozie-ui/wavesurfer closes.

The landscape

FrameworkCommon optionState
React@wavesurfer/reactOfficial, well-maintained hook + component.
Vuecommunity wrappersThin/aging; often just a ref + manual WaveSurfer.create.
SvelteNo maintained wrapper; hand-roll in onMount.
Angularcommunity wrappersSparse/stale; hand-roll with ngAfterViewInit.
SolidNone; hand-roll with onMount/onCleanup.
LitNone; hand-roll a custom element around the engine.

Every "hand-roll" row re-implements the same things: build the engine against a container ref, wire on() events to framework outputs, reconcile prop changes to setOptions/setVolume/zoom, guard the two-way position (and, with regions, a two-way list) against feedback loops, and tear down on unmount. That's exactly the boilerplate Rozie generates — once, from one source, for all six.

What @rozie-ui/wavesurfer gives you

  • Identical surface across all six frameworks — the same props, the same fifteen events, the same two two-way bindings (currentTime + regions), and the same 17-method imperative handle. Learn it once.
  • Idiomatic per-framework output — React forwardRef + hooks, Vue <script setup> + defineModel, Svelte 5 runes, an Angular standalone component, a Solid component, and a Lit custom element. Not a lowest-common-denominator wrapper — real, native code for each.
  • Two-way playback position done right — currentTime binds both ways with a round-trip guard, so playback updates your state and your seeks drive the engine without oscillation.
  • Two-way interactive regionsregions is a second two-way binding: users create/drag/resize/remove selections and your array stays in sync (reconciled by id, guarded against feedback loops), with regionCreated/Updated/Clicked/Removed/In/Out events for the rest.
  • Stateless plugins wired intimeline and hover are one boolean each, live-toggleable (register/unregister on the running engine, no remount) across every target.
  • No CSS import — wavesurfer renders a canvas; there's no stylesheet to remember.

Trade-offs (be honest)

  • Scope is Core + Timeline + Hover + Regions. The spectrogram, minimap, envelope, and record plugins are not yet surfaced. If you need one of those today, reach for it through the engine directly via getWaveSurfer() (the escape hatch), or wait for a follow-up phase.
  • Plugin presence is live, with one asymmetry. timeline/hover register/unregister on the running engine as you toggle them, and regions lazily registers the Regions plugin the first time it becomes an array — none of these require a remount (the regions contents are, as always, fully reactive). There is no live unregister path for regions, though: once the Regions plugin is registered, setting regions back to null does not tear it down. hoverColor/dragToCreateRegions/regionColor are read only when their plugin is (re-)created.
  • React already had a good option. If you're React-only, @wavesurfer/react is excellent. Rozie's value is the other five frameworks getting parity — and one shared API if you ship across several.

See also

Pre-v1.0 — internal monorepo.