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 framework-native props, events, two-way state, and an imperative handle? Today that answer is uneven across ecosystems — which is the gap @rozie-ui/wavesurfer closes.

Research snapshot: 2026-08-10. The wrapper landscape moves; treat these claims as of that date.

The landscape ​

FrameworkCommon optionState
React@wavesurfer/reactOfficial, well-maintained hook + component.
Vuecommunity wrappersThin/low-adoption (well under 1k weekly downloads); often just a ref + manual WaveSurfer.create.
Svelte—No maintained wrapper; hand-roll in onMount.
Angularcommunity wrappersSparse/stale; hand-roll with ngAfterViewInit.
Solid—None; hand-roll with onMount/onCleanup.
Lit—None; 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 is exactly the boilerplate @rozie-ui/wavesurfer ships, identically, everywhere.

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 regions — regions 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 in — timeline 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.

Scope limits ​

  • 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-1.0 — APIs may change between minor versions.