Astro
The @arraypress waveform family ships first-class Astro wrappers for all three 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 |
All three share the same design:
- 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 |
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. |
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.
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';Gotchas
Section titled “Gotchas”- Load the runtime yourself. The wrappers emit markup + a boot script but never inject the core JS/CSS. 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-*.