Skip to content

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 null to “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:inline boot script; nothing in the wrapper touches window or the DOM at build time.
  • View-Transitions-aware. Each wrapper re-runs the core’s idempotent init() on astro:page-load so client-side navigations remount fresh markup.

Install the wrapper alongside the core runtime it wraps (the cores are peer dependencies, so they are not pulled in automatically).

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

Peer 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

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.

src/layouts/Layout.astro
---
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.

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.

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}.

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>
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.

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:

  1. With lazy, the container is emitted as data-waveform-player-lazy (not data-waveform-player), so the core’s auto-init skips it on load.

  2. A single, page-wide IntersectionObserver (deduplicated via window.__wfpLazyMountBound, so many lazy players share one observer) watches every lazy container.

  3. With a 200px rootMargin, the observer promotes a container — swapping data-waveform-player-lazy for data-waveform-player — slightly before it scrolls into view, giving the browser headroom to fetch and decode the audio in time.

  4. 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.

The wrapper is pure markup generation. At build / request time it:

  • reads Astro.props, builds a Record<string, string> of data-* attributes, and renders one <div>;
  • emits an is:inline boot 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.

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.

@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.

The bar depends on the core player, so load both scripts (player first, bar second) and both stylesheets:

src/layouts/Layout.astro
---
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>
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

@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.

Load the player and playlist scripts (player first) and both stylesheets:

src/layouts/Layout.astro
---
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>
---
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.

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.

@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.
Terminal window
npm install @arraypress/waveform-sounds-astro @arraypress/waveform-sounds @arraypress/waveform-player

Import the two stylesheets once, in your layout — the component doesn’t inject CSS:

src/layouts/Layout.astro
---
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']}
/>
<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); }
  • CSP-friendly. It’s a processed Astro <script> — one bundled module emitted as an external file, identical on every page, with no is:inline and no define:vars. Everything per-instance travels in data-*, 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 on astro:after-swap destroys the instances whose element left the page (stopping their audio). A list inside a transition:persist element is carried over and keeps playing. It works with and without <ClientRouter />.
  • It never replaces the page’s player. singlePlay only works between players of the same class, so the script waits for the page’s own scripts (DOMContentLoaded) and uses an existing window.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.

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';
  • 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-player before the bar/playlist script, since both depend on the core player’s global.
  • style ≠ waveform style. In .astro, style is inline CSS — pick the visual style with waveformStyle.
  • No callback props. Wire onLoad/onPlay/etc. via the DOM waveformplayer:* / 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 null is not a reset.