Skip to content

Svelte

The @arraypress waveform family ships four Svelte 5 wrappers, each a thin, typed component over its vanilla core — built with runes ($props, $effect, $derived):

Package Component(s) Wraps
@arraypress/waveform-player-svelte <WaveformPlayer> @arraypress/waveform-player
@arraypress/waveform-bar-svelte <WaveformBar>, <WaveformBarTrigger> @arraypress/waveform-bar
@arraypress/waveform-playlist-svelte <WaveformPlaylist> @arraypress/waveform-playlist
@arraypress/waveform-sounds-svelte <WaveformSounds> @arraypress/waveform-sounds

Every wrapper follows the same design:

  • Prop types are derived from the core’s own typings (WaveformPlayerOptions; WaveformSoundsOptions for the sound list), not re-declared — so they never drift as the core evolves. (The bar is the exception: its core ships no .d.ts, so waveform-bar-svelte hand-declares WaveformBarConfig.)
  • The core JS is dynamically import()ed inside $effect, which only runs in the browser — so the audio + canvas + fetch surface never runs during SSR / SvelteKit prerendering.
  • The imperative API is exposed as Svelte 5 export functions, reached through bind:this — loadTrack, seekTo, playlist navigation, and the rest.
  • CSS is never auto-loaded — you import each core’s stylesheet once at your app entry (e.g. the SvelteKit root +layout.svelte).

svelte (^5.0.0) and @arraypress/waveform-player (^1.8.0) are peer dependencies — you bring them so you control the versions.

Terminal window
npm install @arraypress/waveform-player-svelte @arraypress/waveform-player svelte

Import the core stylesheet once at your app entry. The wrapper does not import it for you — your bundler should own that decision:

src/routes/+layout.svelte
<script>
// also works in main.ts for a plain Vite app
import '@arraypress/waveform-player/dist/waveform-player.css';
</script>
<slot />
<script lang="ts">
import { WaveformPlayer } from '@arraypress/waveform-player-svelte';
</script>
<WaveformPlayer
url="/audio/track.mp3"
title="My Track"
waveformStyle="mirror"
waveformColor={['#fafafa', '#71717a']}
showBPM
ontimeupdate={(currentTime, duration) => {
/* live progress */
}}
/>

WaveformPlayerProps is Omit<WaveformPlayerOptions, …callbacks>, so every player option is accepted as a typed prop — height, samples, barRadius, colorPreset, accessibleSeek, markers, gradient-array colours, and so on. See Player → Options for the full inherited surface. Absent props are not forwarded, so the core’s own defaults apply.

src is a shorthand alias for url (provide one; url wins if both are set). class, style, id, and any other element attribute fall through to the host <div> via HTMLAttributes<HTMLDivElement> — the base class wfp-host is always applied.

Prop Type Notes
url string Audio file URL.
src string Shorthand alias for url.
class string Appended to the always-present base class wfp-host.
style string Inline CSS on the host — e.g. min-height to reserve space before draw.
id string Forwarded to the host <div>.

Every lifecycle event the core exposes is a lowercase callback prop (matching Svelte’s native onclick / oninput convention), each forwarding the live WaveformPlayer 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 Notes
onload (instance) => void Fired once after the waveform is drawn.
onplay (instance) => void Playback started (both audio modes).
onpause (instance) => void Playback paused (both modes).
onend (instance) => void Track ended (external mode synthesizes it at progress ≥ 1).
ontimeupdate (currentTime, duration, instance) => void Progress tick. Same argument order in both modes.
onerror (error, instance) => void Load / decode / audio error.

The component exports its API as Svelte 5 export functions. Grab the instance with bind:this and call them directly:

<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(60)}>Jump to 1:00</button>
<button onclick={() => player.loadTrack('/audio/next.mp3', 'Next Track', 'Artist')}>
Load next
</button>
Method Signature Notes
play () => Promise<void> | undefined Returns the native play() promise in self mode; undefined in external mode.
pause () => void
togglePlay () => void
seekTo (seconds: number) => void Self mode only.
seekToPercent (percent: number) => void Fraction 0..1. Self mode only.
setVolume (volume: number) => void 0..1. Self mode only.
setPlaybackRate (rate: number) => void 0.25..4. Self mode only.
setPlayingState (playing: boolean) => void External mode — push play/pause visual state.
setProgress (currentTime, duration) => void External mode — push the progress overlay from your own clock.
loadTrack (url, title?, artist?, options?) => Promise<void> Swap track at runtime; auto-plays unless options.autoplay === false.
getInstance () => WaveformPlayer | null Escape hatch — the full core API (load, setWaveformData, refreshTheme, container, statics, …).

Calls before the async import resolves are safe no-ops. The typed handle interface is exported as WaveformPlayerExpose for explicit typing.

When pairing with @arraypress/waveform-bar (or any audio controller you own), the player can render visualization only and surrender playback. Drive it via setProgress() / setPlayingState():

<WaveformPlayer
url={track.url}
audioMode="external"
waveformStyle="seekbar"
showInfo={false}
height={32}
/>

Bar — <WaveformBar> + <WaveformBarTrigger>

Section titled “Bar — <WaveformBar> + <WaveformBarTrigger>”

The bar is a singleton: render <WaveformBar> once in your root layout, then scatter <WaveformBarTrigger> elements anywhere to play or queue tracks.

Peer deps: @arraypress/waveform-bar (^1.3.1), @arraypress/waveform-player (^1.7.2), svelte (^5.0.0).

Terminal window
npm install @arraypress/waveform-bar-svelte @arraypress/waveform-bar @arraypress/waveform-player svelte

Render exactly once in your root layout. On mount the component renders a persist host, dynamically imports the core, and calls window.WaveformBar.init(config); on unmount it calls destroy() so route changes don’t leak listeners.

src/routes/+layout.svelte
<script lang="ts">
import { WaveformBar } from '@arraypress/waveform-bar-svelte';
</script>
<slot />
<WaveformBar
config={{
persist: true,
continuous: true,
showQueue: true,
actions: {
favorite: { endpoint: '/api/favorites' },
cart: { endpoint: '/api/cart' },
},
}}
/>
<WaveformBar> prop Type Default Notes
config WaveformBarConfig undefined Passed verbatim to init(). See table below.
persist boolean true Relocate the .waveform-bar element under the host <div> so it survives route changes.
hostId string 'waveform-bar-host' DOM id of the persist host.

class and any other attribute fall through to the host <div> (base class wb-host).

waveform-bar-svelte hand-declares this type (the bar core ships no .d.ts), so this is the source of truth for the typed config surface. Every field is optional.

Group Fields
Behaviour persist, autoResume, continuous, repeat ('off' | 'all' | 'one')
UI toggles showQueue, showPrevNext, showRepeat, showVolume, showMute, showTime, showTrackLink, showMeta, maxMeta (number)
Layout wide, position ('bottom' | 'top'), collapsible, mode ('waveform' | 'classic'), showShuffle, shuffle
Theming defaultArtwork (string | null), theme ('dark' | 'light' | null)
Waveform waveform (boolean), waveformStyle, waveformHeight, barWidth, barSpacing, waveformColor, progressColor, markerColor
Sharing / errors share, shareParam, errorText
Volume / storage volume, storageKey
Server actions actions ({ favorite?, cart? }, each { endpoint, method?, headers? })

<WaveformBarTrigger> emits the data-wb-* attribute contract the bar scans for; the core library handles click delegation. It is polymorphic via as — a <button> by default — and forwards every standard DOM attribute (onclick, data-testid, role, …) to the rendered element via ...rest.

<script lang="ts">
import { WaveformBarTrigger } from '@arraypress/waveform-bar-svelte';
export let track;
</script>
<article>
<h3>{track.title}</h3>
<!-- Default: a <button> with auto play/pause icons -->
<WaveformBarTrigger
url={track.url}
id={track.id}
title={track.title}
artist={track.artist}
artwork={track.cover}
/>
<!-- Append to the queue with custom content -->
<WaveformBarTrigger mode="queue" url={track.url} title={track.title}>
+ Queue
</WaveformBarTrigger>
<!-- Wrap a whole card as the trigger -->
<WaveformBarTrigger as="div" url={track.url} title={track.title} noDefaultIcons>
<div class="card-body"><!-- … --></div>
</WaveformBarTrigger>
</article>

Track data props (WaveformBarTrackData) — each maps 1:1 to a data-wb-* attribute; arrays are JSON-encoded, and absent props emit no attribute:

Prop Type Notes
url string Play target + identity.
id string Defaults to url.
title, artist, album, artwork, link string Display metadata.
duration, bpm string | number
musicalKey string → data-wb-key.
meta string[] Extra metadata chips.
waveform number[] | string Pre-computed peaks or .json URL.
markers WaveformBarMarker[] DJ-mode cue markers ({ time, label, title?, artist?, artwork?, bpm?, key?, color? }).
favorited, inCart boolean Initial action state.

Trigger-specific props:

Prop Type Default Notes
mode 'play' | 'queue' 'play' Immediate play vs. append to queue.
as 'button' | 'a' | 'div' | 'span' 'button' Rendered tag.
href string — Used when as="a".
noDefaultIcons boolean false Suppress the injected play/pause SVGs.
aria-label string auto Auto-generated from title when absent.
class string — Appended to the base class wb-icon-swap.

Slotted children replace the default icons. class, native listeners (onclick), and any other DOM attribute fall through to the rendered element.


A declarative tracks array with optional chapters, an embedded player, and imperative track / chapter navigation.

Peer deps: @arraypress/waveform-playlist (^1.3.0), @arraypress/waveform-player (^1.8.0), svelte (^5.0.0).

Terminal window
npm install @arraypress/waveform-playlist-svelte @arraypress/waveform-playlist @arraypress/waveform-player svelte

Import both stylesheets, and import the player core for its global side effect — the playlist constructs new window.WaveformPlayer(...) for the active track:

src/routes/+layout.svelte
<script>
import '@arraypress/waveform-player'; // registers window.WaveformPlayer
import '@arraypress/waveform-player/dist/waveform-player.css';
import '@arraypress/waveform-playlist/dist/waveform-playlist.css';
</script>
<slot />
<script lang="ts">
import { WaveformPlaylist } from '@arraypress/waveform-playlist-svelte';
</script>
<WaveformPlaylist
continuous
waveformStyle="bars"
tracks={[
{ url: '/audio/a.mp3', title: 'Track A', artist: 'Artist' },
{
url: '/audio/b.mp3',
title: 'Track B',
chapters: [
{ time: 0, label: 'Intro' },
{ time: '1:30', label: 'Verse' },
{ time: 180, label: 'Chorus', color: '#22d3ee' },
],
},
]}
/>

Each track renders into the [data-track] / [data-chapter] markup the playlist constructor parses on mount. The host carries the base class wfp-host.

WaveformPlaylistTrackInput[]:

Field Type Notes
url string Required → data-url.
title string Falls back to the filename if omitted.
artist string Artist row.
artwork string Artwork URL.
album string Media Session metadata.
duration string Display duration, e.g. '3:45'.
markers WaveformPlaylistMarker[] JSON-encoded into data-markers.
chapters WaveformPlaylistChapterInput[] { time: number | string, label, color? } — time accepts seconds or 'M:SS'.

WaveformPlaylistProps combines three groups:

  1. Playlist options picked from WaveformPlaylistOptions: continuous, expandChapters, showDuration, showChapterMarkers, chapterMarkerColor, showPlayState.
  2. Pass-through player options — the full WaveformPlayerOptions visualization / colour / behaviour surface, minus per-track content (url, src, title, artist, artwork, album, markers, waveform), the style alias, the player’s own layout, and the lifecycle callbacks.
  3. Svelte extras — the required tracks, plus class, style, id and other element attributes (fall through to the host).
Prop Type Default Notes
tracks WaveformPlaylistTrackInput[] — Required. An empty array renders an empty playlist (the core skips init).
layout 'list' | 'minimal' 'list' Full track list vs. compact button switcher. Overrides the player’s own layout.
class string — Appended to wfp-host.
style string — Inline CSS on the host.
id string — Forwarded to the host <div>.

Common pass-through player props include waveformStyle, height, colorPreset, waveformColor, progressColor, showBPM, accessibleSeek, and autoplay — see Player → Options for the complete list.

The same lowercase lifecycle callback props as the player (onload, onplay, onpause, onend, ontimeupdate, onerror) are accepted and forwarded to the embedded player; like the player’s, they reach the live instance through reactive closures and never trigger a re-mount. For richer control, reach the embedded player directly through getPlayer().

<script lang="ts">
import { WaveformPlaylist } from '@arraypress/waveform-playlist-svelte';
let playlist: WaveformPlaylist;
export let tracks;
</script>
<WaveformPlaylist bind:this={playlist} {tracks} continuous />
<button onclick={() => playlist.previousTrack()}>Prev</button>
<button onclick={() => playlist.nextTrack()}>Next</button>
<button onclick={() => playlist.selectTrack(0)}>First</button>
Method Signature Notes
selectTrack (index: number) => void Select + load a track by index.
seekToChapter (trackIndex, time) => void Loads the target track first if needed, then seeks.
nextTrack () => void
previousTrack () => void
getPlayer () => unknown | null The embedded WaveformPlayer instance (full player API).
getCurrentTrackIndex () => number
getTracks () => WaveformPlaylistTrack[] Parsed tracks (with resolved element / index).
getInstance () => WaveformPlaylist | null Escape hatch for anything else.

Calls before the async import resolves are safe no-ops. The typed handle interface is exported as WaveformPlaylistExpose.


A searchable, filterable list of sound previews — a sample pack, a preset bank — with a mini waveform per row and one shared audio engine. With sounds, the list is rendered on the server by the core’s own DOM-free renderer, hydration keeps those nodes, and the runtime adopts them instead of rebuilding. See WaveformSounds for what the list does.

Peer deps: @arraypress/waveform-sounds (^0.1.0), @arraypress/waveform-player (^1.24.5), svelte (^5.20.0 — the component uses $props.id(), which 5.20 introduced).

Terminal window
npm install @arraypress/waveform-sounds-svelte @arraypress/waveform-sounds @arraypress/waveform-player svelte

Import both stylesheets, and import the player for its global — the list plays through window.WaveformPlayer, and the wrapper doesn’t import it for you (or pass the class as playerClass):

src/routes/+layout.svelte
<script>
import '@arraypress/waveform-player'; // registers window.WaveformPlayer, the engine
import '@arraypress/waveform-player/styles.css';
import '@arraypress/waveform-sounds/styles.css';
</script>
<slot />
<script lang="ts">
import { WaveformSounds } from '@arraypress/waveform-sounds-svelte';
import pack from '$lib/sounds.json'; // written by waveform-gen --manifest
</script>
<WaveformSounds sounds={pack.sounds} onplay={(sound) => console.log(sound.title)} />
<!-- or, fetched and built in the browser: -->
<WaveformSounds manifest="/sounds.json" />

Write the JSON with waveform-gen --manifest — mind its --base-url, which has to match where the audio is served.

Every WaveformSoundsOptions key except the callbacks is a prop, typed from the core’s index.d.ts (WaveformSoundsProps) — sounds / manifest, player, search, filters, sorts, showCount, menuSearch, idPrefix, urlState, loopToggle, maxTypeChips, pageSize, columns, waveformStyle, waveformColor, progressColor, barWidth, barGap, loop, autoAdvance, arrowAudition, playerOptions, playerClass, strings. See Options for each one. class, id, style and any other attribute land on the host <div>.

The callbacks are lowercase props — onready, onplay, onpause, onend, onfilter, onerror — with the core callbacks’ arguments. They reach the live instance through closures, so a new handler never rebuilds the list. The core’s bubbling waveformsounds:* events fire from the host too.

  • Dropdown ids: idPrefix, else the host’s id, else a per-component id from $props.id() — unique, and identical on the server and during hydration.
  • urlState is runtime-only: the server renders the unfiltered list and the runtime applies the address after hydration.
  • Any change to sounds or an option rebuilds the list over a fresh render, so the new instance never adopts rows the old one re-sorted or hid. A class-only change doesn’t rebuild.

Theme it through style — set --ws-surface (your page background) so the server-rendered first paint is right:

<WaveformSounds {sounds} style="--ws-surface: #0b0b0c" />
<script lang="ts">
import { WaveformSounds } from '@arraypress/waveform-sounds-svelte';
let list: WaveformSounds;
let { sounds } = $props();
</script>
<WaveformSounds bind:this={list} {sounds} />
<button onclick={() => list.setFilter({ type: 'Drum loops' })}>Drum loops</button>
<button onclick={() => list.setSort('bpm')}>By BPM</button>
Method Signature Notes
play (target?, opts?: { at?: number }) => void By index, id or Sound; no target resumes.
pause () => void
toggle (target?) => void
next / previous () => void The next / previous visible sound.
setLoop (on: boolean) => void
setFilter (patch: Partial<SoundsFilter>) => void Merged into the current filter.
clearFilters () => void The sort stays.
setSort (by: SoundsSort) => void
showMore () => void
getInstance () => WaveformSounds | null The core instance.

Calls before the instance mounts are safe no-ops. The handle’s type is exported as WaveformSoundsExpose. bind:instance gives you the core instance itself, reactively (null until mounted).


All four components are SSR / SvelteKit-prerender safe by construction: the host markup (for the playlist, the [data-track] children; for the sound list, the whole list when given sounds) renders on the server, and the browser-only core is dynamically import()ed inside $effect. Because effects never run during SSR, the audio + canvas surface only evaluates after hydration on the client — no import.meta.env.SSR guard or {#if browser} wrapper is needed.

Reserve layout space with the style attribute (e.g. style="min-height: 96px") to avoid a content shift before the waveform draws, and import each core’s CSS once in your root +layout.svelte (for the bar, remember the player-before-bar load order above).

React

The same wrappers, typed for React — React.