@arraypress/waveform-player-svelte
Svelte 5 component wrapper around @arraypress/waveform-player. Typed props for every library option, lifecycle callback props, an exported imperative API (play() / pause() / seekTo() / loadTrack()), and SSR-safe mounting.
The core library stays a zero-dependency vanilla-JS package that works anywhere a <script> tag does. This package adds the framework-native ergonomics Svelte developers expect — built with runes.
<script lang="ts">
import { WaveformPlayer } from '@arraypress/waveform-player-svelte';
</script>
<WaveformPlayer url="/audio/track.mp3" title="My Track" />
Installation
npm install @arraypress/waveform-player-svelte @arraypress/waveform-player svelte
svelte (^5) and @arraypress/waveform-player (^1.8) are peer dependencies — you bring them so you control the versions.
Setup
Import the core library's CSS once in your app entry (SvelteKit +layout.svelte, or main.ts for a plain Vite app):
import '@arraypress/waveform-player/dist/waveform-player.css';
The wrapper does not import the CSS for you — your bundler should own that decision. The library's JS is loaded dynamically inside a $effect (which only runs in the browser), so SSR / SvelteKit prerendering doesn't trip over the browser-only audio APIs.
Usage
Basic
<WaveformPlayer src="/audio/track.mp3" />
Naming note.
srcis shorthand forurl(urlwins if both are set). The visual style iswaveformStyle— notstyle, which (as on any element) is CSS.class,style,id, and other element attributes fall through to the host element; the base classwfp-hostis always applied.
With metadata + chosen style
<WaveformPlayer
url="/audio/track.mp3"
title="Midnight Dreams"
artist="The Wavelength"
artwork="/img/cover.jpg"
waveformStyle="bars"
barWidth={3}
barSpacing={1}
height={80}
/>
Pre-computed peaks (recommended for catalogues)
<WaveformPlayer url="/audio/track.mp3" waveform="/peaks/track.json" />
Generate the JSON at build time with @arraypress/waveform-gen. Removes the Web Audio decode cost (~1–5 s per file) from the render path.
Chapter markers
<script lang="ts">
import { WaveformPlayer, type WaveformMarker } from '@arraypress/waveform-player-svelte';
const markers: WaveformMarker[] = [
{ time: 0, label: 'Intro' },
{ time: 60, label: 'Main topic', color: '#a855f7' },
{ time: 600, label: 'Q&A' },
];
</script>
<WaveformPlayer url="/audio/podcast.mp3" {markers} />
Callback props
Every lifecycle event the core exposes is a lowercase callback prop, each forwarding the live instance:
<WaveformPlayer
url="/audio/track.mp3"
onload={(i) => console.log('loaded', i)}
onplay={() => console.log('playing')}
onpause={() => console.log('paused')}
ontimeupdate={(currentTime, duration) => console.log(`${currentTime}s / ${duration}s`)}
onend={() => console.log('finished')}
onerror={(err) => console.error('audio failed:', err)}
/>
| Prop | Signature |
|---|---|
onload |
(instance) => void |
onplay |
(instance) => void |
onpause |
(instance) => void |
onend |
(instance) => void |
ontimeupdate |
(currentTime: number, duration: number, instance) => void |
onerror |
(error: Error, instance) => void |
Callback props don't trigger re-mounts — they reach the live instance through reactive closures, so changing a handler never tears the player down.
Imperative control via bind:this
For "play this track when X happens" flows where wiring through props is awkward:
<script lang="ts">
import { WaveformPlayer } from '@arraypress/waveform-player-svelte';
let player: WaveformPlayer;
</script>
<WaveformPlayer bind:this={player} url="/audio/track.mp3" />
<button onclick={() => player.togglePlay()}>Play / Pause</button>
<button onclick={() => player.seekTo(30)}>Jump to 0:30</button>
<button onclick={() => player.setVolume(0.5)}>Vol 50%</button>
The exported methods (play(), pause(), togglePlay(), seekTo(), seekToPercent(), setVolume(), setPlaybackRate(), setPlayingState(), setProgress(), loadTrack()) pass straight through to the underlying instance. player.getInstance() returns the raw instance for anything not surfaced yet.
External audio mode
When pairing with @arraypress/waveform-bar (or any audio controller you own), the player can render visualisation only and surrender playback:
<WaveformPlayer
url={track.url}
audioMode="external"
waveformStyle="seekbar"
showInfo={false}
/>
Drive the visualisation from your controller via player.setProgress(currentTime, duration) and player.setPlayingState(playing).
How prop changes are handled
When any prop the core library uses at construction time changes (url, audioMode, waveformStyle, markers, colours, sizing, …), the wrapper destroys the existing instance and creates a new one with the updated options — driven by a single $effect that reads those props. That's simpler and more correct than diffing every option, and the core has built-in caches (waveform peaks keyed by URL) that make same-URL re-mounts cheap.
Props
Every library option surfaces as a typed prop. Absent props are not forwarded, so the core library's own defaults apply. See src/lib/types.ts and src/lib/WaveformPlayer.svelte for the full list, and the core docs for per-option behaviour. Highlights:
- Audio source —
url,src(alias),audioMode,preload - Waveform —
waveformStyle,height,samples,barWidth,barSpacing,barRadius,waveform - Colours —
colorPreset,waveformColor,progressColor(strings, orstring[]for gradients). The DOM chrome (button, title, meta text) is themed via CSS variables —--wfp-button-color,--wfp-text-color,--wfp-text-secondary-color— not JS options; override them in your own CSS. - Playback / UI —
playbackRate,showPlaybackSpeed,playbackRates,showControls,showInfo,showTime,showHoverTime,showBPM,buttonAlign,accessibleSeek,seekLabel,errorText - Markers / metadata —
markers,showMarkers,title,artist,artwork,album - Behaviour / icons —
autoplay,singlePlay,playOnSeek,enableMediaSession,playIcon,pauseIcon
TypeScript
import type {
WaveformPlayerProps,
WaveformPlayerCallbacks,
WaveformPlayerExpose,
WaveformStyle,
WaveformMarker,
WaveformPeaks,
ColorPreset,
AudioMode,
AudioPreload,
ButtonAlign,
} from '@arraypress/waveform-player-svelte';
The shared option types are re-exported straight from the core library, so they can never drift out of sync. The package ships .svelte + .d.ts (generated by svelte-package).
Testing
npm test # one-shot
npm run test:watch
npm run typecheck # svelte-check
npm run build # svelte-package → dist/
The core library is mocked at the module boundary (jsdom has no Web Audio API). Tests cover mount, option pass-through, the src → url alias, boolean-prop omission, callback forwarding, destroy-on-unmount, identity-prop re-mount, and the exported imperative API.
License
MIT ArrayPress