Astro
The @arraypress waveform family ships first-class Astro wrappers for all four runtimes. Each one turns the core library’s data-* attribute contract into a typed .astro component: you pass camelCase props, the wrapper stringifies them into the exact attributes the framework-agnostic core scans for, and the core’s auto-initialiser mounts the UI on the client.
| Package | Component(s) | Wraps |
|---|---|---|
@arraypress/waveform-player-astro |
<WaveformPlayer> |
@arraypress/waveform-player |
@arraypress/waveform-bar-astro |
<WaveformBar>, <WaveformBarTrigger> |
@arraypress/waveform-bar |
@arraypress/waveform-playlist-astro |
<WaveformPlaylist> |
@arraypress/waveform-playlist |
@arraypress/waveform-sounds-astro |
<WaveformSounds> |
@arraypress/waveform-sounds |
The player, bar and playlist wrappers share the same design (the sound list differs in two ways — it server-renders the list and loads its own runtime):
- Props mirror core options 1:1 (camelCase). The component handles the kebab-case
data-*conversion under the hood. - Omission preserves defaults. An omitted prop emits no attribute, so the core applies its own internal default. Never pass
nullto “reset” a value — just leave the prop out. - The wrapper does not load the runtime. You load the core JS + CSS once in a layout, so you stay in control of CDN vs. self-hosted vs. bundled.
- SSR-safe. The server renders a plain container plus a small
is:inlineboot script; nothing in the wrapper toucheswindowor the DOM at build time. - View-Transitions-aware. Each wrapper re-runs the core’s idempotent
init()onastro:page-loadso client-side navigations remount fresh markup.
Install
Section titled “Install”Install the wrapper alongside the core runtime it wraps (the cores are peer dependencies, so they are not pulled in automatically).
npm install @arraypress/waveform-player-astro @arraypress/waveform-playerpnpm add @arraypress/waveform-player-astro @arraypress/waveform-playeryarn add @arraypress/waveform-player-astro @arraypress/waveform-playerbun add @arraypress/waveform-player-astro @arraypress/waveform-playerPeer dependency ranges per wrapper:
| Wrapper | Core peers | Astro |
|---|---|---|
waveform-player-astro |
@arraypress/waveform-player@^1.8.0 |
^6.0.0 || ^7.0.0 |
waveform-bar-astro |
@arraypress/waveform-bar@^1.3.1, @arraypress/waveform-player@^1.7.2 |
^6.0.0 || ^7.0.0 |
waveform-playlist-astro |
@arraypress/waveform-playlist@^1.3.0, @arraypress/waveform-player@^1.8.0 |
^6.0.0 || ^7.0.0 |
waveform-sounds-astro |
@arraypress/waveform-sounds@^0.1.0, @arraypress/waveform-player@^1.24.5 |
^6.0.0 || ^7.0.0 |
Load the runtime once (in a layout)
Section titled “Load the runtime once (in a layout)”The wrappers emit markup and a boot script, but they deliberately do not inject the core library’s JS or CSS — you decide where and how it loads. Pull the assets into your root layout’s <head> once, and every <WaveformPlayer> on every page is covered.
The cleanest pattern: import the CSS so Astro bundles and hashes it, and import the minified JS with the ?url suffix so you get a hashed URL to mount via an is:inline script tag.
---import '@arraypress/waveform-player/dist/waveform-player.css';import wfpJsUrl from '@arraypress/waveform-player/dist/waveform-player.min.js?url';---<html lang="en"> <head> <meta charset="utf-8" /> <script src={wfpJsUrl} is:inline></script> </head> <body> <slot /> </body></html>The core’s global window.WaveformPlayer is what every wrapper’s boot script looks for. If it is missing when a lazy player tries to mount, the wrapper retries for ~1s and then logs a one-time [WaveformPlayerAstro] warning rather than failing silently — so a missing script tag is loud, not mysterious.
The player component
Section titled “The player component”Import the default export and drop it anywhere. url is the one prop you almost always pass; everything else is optional and falls through to the core’s defaults when omitted.
---import WaveformPlayer from '@arraypress/waveform-player-astro';---<WaveformPlayer url="/audio/track.mp3" title="Midnight Drive" artist="The Synthwaves" waveformStyle="mirror" waveformColor={['#fafafa', '#71717a']} showBPM/>That renders a single <div class="wfp-host" data-waveform-player data-url="/audio/track.mp3" …> plus an inline init script. On the client, the core’s auto-initialiser finds the container and builds the player into it.
Prop reference
Section titled “Prop reference”Props are inherited verbatim from the core’s WaveformPlayerOptions, so the full option list is the source of truth. The most common props:
| Prop | Type | Notes |
|---|---|---|
url |
string |
Audio source. Alias src is also accepted; url wins if both are set. |
waveformStyle |
'bars' | 'mirror' | 'line' | 'blocks' | 'dots' | 'seekbar' |
Visual style. Use this, not style (see below). |
height |
number |
Waveform height in px. |
samples |
number |
Source peak resolution decoded. |
waveform |
number[] | string |
Pre-computed peaks: an array, a CSV/JSON string, or a .json URL. Arrays are JSON-encoded for you. |
waveformColor / progressColor |
string | string[] |
A CSS colour or an array of gradient stops (top→bottom). Arrays are JSON-encoded into the data-* attribute automatically. |
colorPreset |
'dark' | 'light' |
Forces a theme; omit for auto-detection. |
markers |
Array<{ time: number; label: string; color?: string }> |
Cue markers; JSON-encoded for you. |
audioMode |
'self' | 'external' |
'self' owns an <audio>; 'external' is visualization-only. |
crossOrigin |
'anonymous' | 'use-credentials' |
CORS mode for the <audio>. Unset by default (native <audio> behavior); set only if you need it. |
showBPM, showControls, showInfo, showTime |
boolean |
UI toggles. Astro boolean shorthand works: showBPM ≡ showBPM={true}. |
Astro-specific props
Section titled “Astro-specific props”On top of the inherited options, the component adds four extras:
| Prop | Type | Default | Purpose |
|---|---|---|---|
lazy |
boolean |
false |
Defer mounting until the player nears the viewport (see below). |
id |
string |
— | DOM id forwarded to the container; target it via WaveformPlayer.getInstance(id). |
class |
string |
— | Extra class names. The base class wfp-host is always applied. |
style |
string |
— | Inline CSS on the container — handy for a min-height to reserve layout space before the canvas draws. |
Two core options behave differently in Astro
Section titled “Two core options behave differently in Astro”Lifecycle callbacks are not props. A server-rendered component emits static HTML with nothing to attach a JS function to, so onLoad, onPlay, onPause, onEnd, onError, and onTimeUpdate are intentionally absent from the prop type. Wire lifecycle handling to the DOM waveformplayer:* events from a client script instead:
<WaveformPlayer id="hero" url="/audio/track.mp3" />
<script> document.getElementById('hero')?.addEventListener('waveformplayer:ended', (e) => { console.log('finished', e.detail.url); });</script>Pre-computed peaks (recommended for catalogues)
Section titled “Pre-computed peaks (recommended for catalogues)”Decoding audio with Web Audio is expensive, especially across a grid. Ship a sibling .json peaks file and pass it as waveform to skip the decode entirely:
<WaveformPlayer url="/audio/track.mp3" waveform="/peaks/track.json" waveformStyle="bars" barWidth={3} showBPM/>The peaks file may embed { peaks, markers }; the core applies embedded markers automatically.
Lazy IntersectionObserver mounting
Section titled “Lazy IntersectionObserver mounting”Mounting 15+ players on one page means 15+ concurrent fetch() + decodeAudioData jobs firing on load — janky, and prone to “Unable to load audio” stutters. Pass lazy to defer each player until it approaches the viewport.
---import WaveformPlayer from '@arraypress/waveform-player-astro';const previews = await getPreviews();---<div class="grid"> {previews.map((p) => ( <WaveformPlayer url={p.url} title={p.title} waveform={p.peaks} lazy /> ))}</div>How it works:
-
With
lazy, the container is emitted asdata-waveform-player-lazy(notdata-waveform-player), so the core’s auto-init skips it on load. -
A single, page-wide
IntersectionObserver(deduplicated viawindow.__wfpLazyMountBound, so many lazy players share one observer) watches every lazy container. -
With a
200pxrootMargin, the observer promotes a container — swappingdata-waveform-player-lazyfordata-waveform-player— slightly before it scrolls into view, giving the browser headroom to fetch and decode the audio in time. -
It then calls
window.WaveformPlayer.init()to mount just that container. The buffered rootMargin is why the player is ready, not loading, by the time the user sees it.
The observer is re-attached on astro:page-load, so lazy mounts rendered by a client-side navigation are picked up too. If the core script has not installed window.WaveformPlayer yet, the boot script polls (bounded, ~1s) instead of giving up.
SSR safety
Section titled “SSR safety”The wrapper is pure markup generation. At build / request time it:
- reads
Astro.props, builds aRecord<string, string>ofdata-*attributes, and renders one<div>; - emits an
is:inlineboot script (the lazy observer, or the View-Transitions re-init).
It never references window, document, IntersectionObserver, or the AudioContext during render — those names appear only inside the inline scripts, which run on the client. That makes the component safe in any Astro output mode (static, server, hybrid) and inside .astro files rendered on the edge.
Astro View Transitions
Section titled “Astro View Transitions”The core auto-initialises on DOMContentLoaded only. Under View Transitions, client-side navigations swap the DOM without re-firing that event, so a freshly-rendered non-lazy player would never mount after a navigation.
Every non-lazy <WaveformPlayer> ships a tiny boot script that re-runs the core’s idempotent WaveformPlayer.init() on each astro:page-load. It deduplicates via window.__wfpInitBound (one shared listener), and the core skips any container already flagged data-waveform-initialized, so re-running it only mounts the new ones. No configuration required — it just works once you enable View Transitions in your layout.
The bar
Section titled “The bar”@arraypress/waveform-bar-astro exposes two components:
<WaveformBar>— the persistent bottom-bar singleton. Render it once in your root layout.<WaveformBarTrigger>— a polymorphic play/queue trigger you scatter across cards, rows, and modals.
Setup — load both runtimes
Section titled “Setup — load both runtimes”The bar depends on the core player, so load both scripts (player first, bar second) and both stylesheets:
---import '@arraypress/waveform-player/dist/waveform-player.css';import '@arraypress/waveform-bar/dist/waveform-bar.css';import wfpJsUrl from '@arraypress/waveform-player/dist/waveform-player.min.js?url';import wbJsUrl from '@arraypress/waveform-bar/dist/waveform-bar.min.js?url';import { WaveformBar } from '@arraypress/waveform-bar-astro';---<html lang="en"> <head> <script src={wfpJsUrl} is:inline></script> <script src={wbJsUrl} is:inline></script> </head> <body> <slot /> <WaveformBar config={{ persist: true, continuous: true, showQueue: true }} /> </body></html><WaveformBar> — the singleton mount
Section titled “<WaveformBar> — the singleton mount”| Prop | Type | Default | Purpose |
|---|---|---|---|
config |
WaveformBarConfig |
{} |
Passed verbatim to window.WaveformBar.init(config). Omit for all defaults. |
persist |
boolean |
true |
Render the host with transition:persist so the bar survives View Transitions intact. |
hostId |
string |
'waveform-bar-host' |
DOM id of the persistent host <div>. |
class |
string |
— | Extra class names on the host (base class wb-host). |
The component renders one persistent host <div> and an inline script that calls WaveformBar.init(config) exactly once per session (guarded by window.__wbAstroInitBound and window.__apWaveformBarInited). This once-only behaviour is deliberate: the core’s init() destroys and rebuilds the bar on each call, which would restart the playing audio and defeat transition:persist. After init, the script relocates the library’s .waveform-bar element into the persist host so navigations keep it on screen.
A representative slice of WaveformBarConfig (every field optional):
| Field | Type | Default | |
|---|---|---|---|
persist |
boolean |
true |
Save queue/position to storage; resume across loads. |
autoResume |
boolean |
true |
Resume playback after a navigation. |
continuous |
boolean |
true |
Auto-advance to the next queued track. |
repeat |
'off' | 'all' | 'one' |
'off' |
Initial repeat mode. |
showQueue |
boolean |
true |
Show the queue toggle + panel. |
waveformStyle |
WaveformStyle |
'mirror' |
Waveform style inside the bar. |
maxMeta |
number |
3 |
Cap on metadata chips (BPM/key/tags). |
theme |
'dark' | 'light' | null |
null |
Forced theme; null auto-detects. |
position |
'bottom' | 'top' |
'bottom' |
Which edge the bar docks to. |
mode |
'waveform' | 'classic' |
'waveform' |
'classic' = Spotify-style centre layout + seek bar. |
share |
boolean |
false |
Show a “copy share link” button with a timestamp param. |
actions |
{ favorite?, cart? } |
— | Favourite / cart endpoint config (see caveat). |
<WaveformBarTrigger> — play / queue triggers
Section titled “<WaveformBarTrigger> — play / queue triggers”A polymorphic element that emits the data-wb-* attribute contract the bar’s runtime click-delegation scans for. It defaults to a <button> (keyboard focus, Space/Enter, accessible role for free) and injects a play/pause SVG pair the bar toggles per track. Pass children to replace those defaults.
---import { WaveformBarTrigger } from '@arraypress/waveform-bar-astro';const { product } = Astro.props;---<article class="product-card"> <img src={product.cover} alt="" /> <h3>{product.title}</h3>
<WaveformBarTrigger url={product.previewUrl} id={product.id} title={product.title} artist={product.artist} artwork={product.cover} waveform={product.peaks} class="card-play-btn" />
<WaveformBarTrigger mode="queue" url={product.previewUrl} title={product.title}> + Queue </WaveformBarTrigger></article>Key props (every track field optional except url):
| Prop | Type | Default | Becomes |
|---|---|---|---|
mode |
'play' | 'queue' |
'play' |
data-wb-play / data-wb-queue |
as |
'button' | 'a' | 'div' | 'span' |
'button' |
the rendered tag |
url |
string |
— | data-wb-url (the play target / identity) |
id |
string |
falls back to url |
data-wb-id |
title, artist, album, artwork, link |
string |
— | data-wb-title, -artist, -album, -artwork, -link |
duration, bpm |
string | number |
— | data-wb-duration, -bpm |
key |
string |
— | data-wb-key |
meta |
string[] |
— | data-wb-meta (JSON) |
waveform |
number[] | string |
— | data-wb-waveform (arrays JSON-encoded) |
markers |
WaveformBarMarker[] |
— | data-wb-markers (JSON) — DJ-mode chapter markers |
favorited, inCart |
boolean |
— | data-wb-favorited, -in-cart |
href |
string |
— | rendered only when as="a" |
ariaLabel |
string |
auto from title |
accessible name |
noDefaultIcons |
boolean |
false |
suppress the injected play/pause SVGs |
The playlist
Section titled “The playlist”@arraypress/waveform-playlist-astro wraps the playlist runtime, which embeds a self-mode player and renders one row per track with optional chapters. The wrapper turns the nested [data-track] / [data-chapter] markup into a single typed tracks array.
Setup — load both runtimes
Section titled “Setup — load both runtimes”Load the player and playlist scripts (player first) and both stylesheets:
---import '@arraypress/waveform-player/dist/waveform-player.css';import '@arraypress/waveform-playlist/dist/waveform-playlist.css';import wfpJsUrl from '@arraypress/waveform-player/dist/waveform-player.min.js?url';import wfplJsUrl from '@arraypress/waveform-playlist/dist/waveform-playlist.js?url';---<head> <script src={wfpJsUrl} is:inline></script> <script src={wfplJsUrl} is:inline></script></head>Example — podcast with chapters
Section titled “Example — podcast with chapters”---import WaveformPlaylist from '@arraypress/waveform-playlist-astro';---<WaveformPlaylist continuous showPlayState tracks={[ { url: '/audio/ep42.mp3', title: 'Episode 42', artist: 'with Dr. Sarah Chen', artwork: '/img/ep42.jpg', duration: '48:12', chapters: [ { time: 0, label: 'Intro' }, { time: 330, label: 'Main Topic' }, { time: 2700, label: 'Q&A' }, ], }, ]}/>This renders a [data-waveform-playlist] container with one [data-track] child per entry and one [data-chapter] child per chapter.
Playlist-specific props
Section titled “Playlist-specific props”| Prop | Type | Purpose |
|---|---|---|
tracks |
WaveformPlaylistTrackInput[] |
Required. One entry per track. |
layout |
'list' | 'minimal' |
Playlist layout. |
continuous |
boolean |
Auto-advance to the next track. |
expandChapters |
boolean |
Expand the chapter list by default. |
showDuration |
boolean |
Show per-track durations. |
showChapterMarkers |
boolean | null |
Render chapter markers on the waveform; null (default) uses the content-aware smart default. |
chapterMarkerColor |
string |
Chapter marker line colour. |
showPlayState |
boolean |
Show the playing/paused state per row. |
Each WaveformPlaylistTrackInput accepts:
| Field | Type | Becomes |
|---|---|---|
url |
string |
data-url |
title, artist, artwork, album |
string |
data-title, -artist, -artwork, -album |
duration |
string |
data-duration (free-form display, e.g. '3:45') |
markers |
{ time: number; label?: string; color?: string }[] |
data-markers (JSON) — waveform cue markers |
chapters |
{ time: number; label: string; color?: string }[] |
nested [data-chapter] rows |
The component also forwards the embedded player’s visual options (waveformStyle, height, samples, barWidth, the colour props, showBPM, accessibleSeek, …) straight onto the container, where the playlist reads and passes them to the player it builds.
Both lazy mounting (lazy → data-waveform-playlist-lazy, deduped via window.__wfplLazyMountBound) and View-Transitions re-init (deduped via window.__wfplInitBound) work identically to the player component.
The sound list
Section titled “The sound list”@arraypress/waveform-sounds-astro wraps WaveformSounds, the searchable, filterable list of sound previews. It breaks from the other three wrappers in two ways:
- It renders the whole list on the server. Given
sounds, the component writes the toolbar, the chips and every row with the core’s DOM-free renderer (@arraypress/waveform-sounds/render); the browser runtime adopts that markup instead of rebuilding it, so the list is readable and crawlable before any script runs. - It loads its own JavaScript. The component ships one processed (bundled)
<script>that loads the sounds runtime, and the player too unless the page already has one. You only add the stylesheets.
npm install @arraypress/waveform-sounds-astro @arraypress/waveform-sounds @arraypress/waveform-playerImport the two stylesheets once, in your layout — the component doesn’t inject CSS:
---import '@arraypress/waveform-player/styles.css';import '@arraypress/waveform-sounds/styles.css';---Example — from an array, server-rendered
Section titled “Example — from an array, server-rendered”---import WaveformSounds from '@arraypress/waveform-sounds-astro';---<WaveformSounds sounds={[ { url: '/previews/kick-01.mp3', title: 'Kick 01', type: 'Drums', bpm: 128, key: 'F minor', duration: 2.4 }, { url: '/previews/bass-04.mp3', title: 'Bass Loop 04', type: 'Bass', bpm: 128, key: 'Fmin', peaks: 'a1b2c3…' }, ]} pageSize={100} columns={['type', 'bpm', 'key']}/>Example — from a manifest
Section titled “Example — from a manifest”<WaveformSounds manifest="/previews/sounds.json" player="strip" />With only manifest (and no sounds), the component renders an empty container carrying data-manifest, and the runtime fetches and renders the list in the browser. To server-render a manifest instead, read it at build time and pass its sounds array:
---import WaveformSounds from '@arraypress/waveform-sounds-astro';import pack from '../../public/previews/sounds.json';---<WaveformSounds sounds={pack.sounds} />Write the manifest with waveform-gen --manifest — and set its --base-url to where the audio is served (/previews/ above), or the URLs point at the site root.
Every core option is a prop, typed from the core’s WaveformSoundsOptions, and emitted as the data-* attribute the core reads — an omitted prop emits nothing, so the core’s default applies. See Options for each one.
| Prop | Type | Notes |
|---|---|---|
sounds |
SoundInput[] |
Rendered on the server. An empty array renders an empty list (the manifest isn’t fetched). |
manifest |
string |
Fetched in the browser when there are no sounds. |
player |
'inline' | 'strip' |
Always emitted (resolved the way the core resolves it). |
search, showCount, loopToggle, loop, autoAdvance, arrowAudition |
boolean |
Emitted as "true" / "false". |
filters, sorts, columns |
string[] |
Comma-separated; an empty array is emitted as "" = none. |
menuSearch, pageSize, maxTypeChips, barWidth, barGap |
number |
|
waveformStyle, waveformColor, progressColor |
string |
|
idPrefix |
string |
Defaults to id, matching the core’s own client render. |
urlState |
boolean | string |
"true" / "false", or the prefix. |
strings, playerOptions |
object |
Emitted as JSON (functions inside playerOptions don’t survive). |
id |
string |
Container id — for WaveformSounds.getInstance('#id'). |
class |
string |
Added to waveform-sounds waveform-sounds--<player>. |
Not available: the on* callbacks (a function can’t cross into server-rendered HTML — listen for the bubbling waveformsounds:* events instead) and playerClass (the page’s window.WaveformPlayer is used, see below).
<WaveformSounds id="pack" manifest="/previews/sounds.json" />
<script> document.addEventListener('waveformsounds:play', (e) => { console.log('playing', e.detail.sound.title); });</script>For the imperative API, window.WaveformSounds.getInstance('#pack') returns the instance. The client script creates it, so call it from an event handler or after waveformsounds:ready.
Set --ws-surface to your page background — the runtime detects it, but a server-rendered list paints before any script runs:
.waveform-sounds { --ws-surface: var(--color-bg); }The client script
Section titled “The client script”- CSP-friendly. It’s a processed Astro
<script>— one bundled module emitted as an external file, identical on every page, with nois:inlineand nodefine:vars. Everything per-instance travels indata-*, so a strict hash-based Content-Security-Policy needs no extra hashes for it. - View Transitions. It initialises on load and on every
astro:page-load, and onastro:after-swapdestroys the instances whose element left the page (stopping their audio). A list inside atransition:persistelement is carried over and keeps playing. It works with and without<ClientRouter />. - It never replaces the page’s player.
singlePlayonly works between players of the same class, so the script waits for the page’s own scripts (DOMContentLoaded) and uses an existingwindow.WaveformPlayer— a persistent WaveformBar’s, or one from a<script>tag. Only when there is none does it load the bundled@arraypress/waveform-player(a separate chunk, by dynamic import). A player your code loads later than that, from its own dynamic import, can’t be detected — load it from a static<script>or import instead.
TypeScript
Section titled “TypeScript”Each wrapper re-exports the shared core types so you can import them from one place, and derives its prop interface from the core options via Omit<> / Pick<> — so when the core adds an option, the typed props track it automatically with no manual edit.
import type { WaveformPlayerProps, WaveformStyle, WaveformMarker, WaveformPeaks, ColorPreset,} from '@arraypress/waveform-player-astro';
import type { WaveformBarProps, WaveformBarConfig, WaveformBarTriggerProps, WaveformBarTrackData, WaveformBarMarker,} from '@arraypress/waveform-bar-astro';
import type { WaveformPlaylistProps, WaveformPlaylistTrackInput,} from '@arraypress/waveform-playlist-astro';
import type { WaveformSoundsProps, SoundInput, SoundsManifest, WaveformSoundsEventMap,} from '@arraypress/waveform-sounds-astro';Gotchas
Section titled “Gotchas”- Load the runtime yourself (player, bar, playlist). Those wrappers emit markup + a boot script but never inject the core JS/CSS. (
<WaveformSounds>loads its own JS; every wrapper leaves the CSS to you.) Forget the script tag and you get a one-time[WaveformPlayerAstro]/[WaveformBarAstro]/[WaveformPlaylistAstro]console warning. - Script order matters for the bar and playlist: load
@arraypress/waveform-playerbefore the bar/playlist script, since both depend on the core player’s global. style≠ waveform style. In.astro,styleis inline CSS — pick the visual style withwaveformStyle.- No callback props. Wire
onLoad/onPlay/etc. via the DOMwaveformplayer:*/waveformbar:*events from a client script. - Render
<WaveformBar>once. It is a singleton; a second instance re-inits the bar and restarts audio. - Omit, don’t null. An omitted prop emits no attribute and lets the core default win; passing
nullis not a reset.
See also
Section titled “See also”- Installation — runtime install and CDN options.
- Player options · Player events — the full inherited option and event surface.
- React — the same wrappers for React, passing constructor options instead of
data-*.