# Vue

> Typed Vue 3 components.

The `@arraypress` waveform family ships three Vue 3 wrappers, each a thin, typed component over its vanilla core:

| Package | Component(s) | Wraps |
| --- | --- | --- |
| `@arraypress/waveform-player-vue` | `<WaveformPlayer>` | [`@arraypress/waveform-player`](/player/options/) |
| `@arraypress/waveform-bar-vue` | `<WaveformBar>`, `<WaveformBarTrigger>` | [`@arraypress/waveform-bar`](/extensions/bar/) |
| `@arraypress/waveform-playlist-vue` | `<WaveformPlaylist>` | [`@arraypress/waveform-playlist`](/extensions/playlist/) |

Every wrapper follows the same design:

- **Each is a `defineComponent`** with a render function — no `.vue` compile step, dual ESM/CJS builds, and a hand-written `.d.ts`. Core options surface as **props**, lifecycle events as **emits**, and the instance as a **template ref**.
- **Prop types are derived from the core's `WaveformPlayerOptions`**, not re-declared — so they never drift as the core evolves.
- **The core JS is dynamically `import()`ed inside `onMounted`**, so the audio + canvas + `fetch` surface never runs during SSR / Nuxt.
- **Every optional prop is declared `{ type: …, default: undefined }`.** An absent prop is *never forwarded* to the core, so the core's own default applies — you only pass the props you want to override.
- **CSS is never auto-loaded** — you import each core's stylesheet once at your app entry.

:::caution[Use `waveformStyle`, not `style`]
On all three components, `style` (and `class`, `id`) is a standard HTML attribute that **falls through to the host `<div>`** via Vue's attribute inheritance — use it to reserve layout height, etc. The base class (`wfp-host` / `wb-host`) is always merged in. The core's `style` alias for the *visual* waveform style is intentionally omitted from the prop type. To pick a visual style use **`waveformStyle`** / `waveform-style` (`'bars' | 'mirror' | 'line' | 'blocks' | 'dots' | 'seekbar'`).
:::

---

## Player — `<WaveformPlayer>`

### Install

`vue` and `@arraypress/waveform-player` are peer dependencies (Vue `^3.5.0`; core player `^1.8.0`).

<Tabs syncKey="pkg">

<TabItem label="npm">

```bash
npm install @arraypress/waveform-player-vue @arraypress/waveform-player vue
```

</TabItem>
<TabItem label="pnpm">

```bash
pnpm add @arraypress/waveform-player-vue @arraypress/waveform-player vue
```

</TabItem>
<TabItem label="yarn">

```bash
yarn add @arraypress/waveform-player-vue @arraypress/waveform-player vue
```

</TabItem>
<TabItem label="bun">

```bash
bun add @arraypress/waveform-player-vue @arraypress/waveform-player vue
```

</TabItem>

</Tabs>

Import the core stylesheet **once** at your app entry:

```ts
// main.ts / app entry

```

### Quick start

```vue
<script setup lang="ts">

</script>

<template>
  <WaveformPlayer
    url="/audio/track.mp3"
    title="My Track"
    waveform-style="mirror"
    :waveform-color="['#fafafa', '#71717a']"
    show-bpm
    @timeupdate="(currentTime, duration) => { /* live progress */ }"
  />
</template>
```

`src` is the core's shorthand alias for `url` (`url` wins if both are set). Provide one of the two.

### Props

Every core player option is accepted as a typed prop — `url`, `src`, `audioMode`, `height`, `samples`, `barWidth`, `barSpacing`, `barRadius`, `colorPreset`, gradient-array colours (`waveformColor`, `progressColor`), `accessibleSeek`, `markers`, `showBPM`, and so on. The prop type `WaveformPlayerProps` is `Omit<WaveformPlayerOptions, …callbacks>`, so it tracks the core automatically. See [Player → Options](/player/options/) for the full inherited surface.

Because every optional prop defaults to `undefined`, props you don't pass are not forwarded and the core's default wins.

### Events

The six core lifecycle callbacks surface as **emits** (not callback props) — each forwards the live `WaveformPlayer` instance:

```vue
<WaveformPlayer
  url="/audio/track.mp3"
  @load="(player) => console.log('loaded', player)"
  @play="() => console.log('playing')"
  @pause="() => console.log('paused')"
  @timeupdate="(currentTime, duration) => console.log(`${currentTime} / ${duration}`)"
  @end="() => console.log('finished')"
  @error="(err) => console.error('audio failed:', err)"
/>
```

| Emit | Payload | Notes |
| --- | --- | --- |
| `@load` | `(instance)` | Fired after the waveform is drawn. |
| `@play` | `(instance)` | Playback started (both audio modes). |
| `@pause` | `(instance)` | Playback paused (both modes). |
| `@end` | `(instance)` | Track ended (external mode synthesizes it at progress ≥ 1). |
| `@timeupdate` | `(currentTime, duration, instance)` | Progress tick. Same argument order in both modes. |
| `@error` | `(error, instance)` | Load / decode / audio error. |

`emit` is stable, so listeners always reach the latest handler without tearing the player down. Only construction-time props (`url`, `audioMode`, visualization options, …) trigger a re-mount.

### Imperative API

Grab the instance with a template `ref` typed as `WaveformPlayerExpose`. Methods are reached through `defineExpose`:

```vue
<script setup lang="ts">

  WaveformPlayer,
  type WaveformPlayerExpose,
} from '@arraypress/waveform-player-vue';

const player = ref<WaveformPlayerExpose>();
</script>

<template>
  <WaveformPlayer ref="player" url="/audio/track.mp3" />
  <button @click="player?.togglePlay()">Play / Pause</button>
  <button @click="player?.seekTo(60)">Jump to 1:00</button>
  <button @click="player?.loadTrack('/audio/next.mp3', 'Next Track', 'Artist')">
    Load next
  </button>
</template>
```

| 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` | Clamped `0.5..2`. 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`. |
| `instance` | `readonly WaveformPlayer \| null` | Escape hatch — full core API (`load`, `setWaveformData`, `refreshTheme`, `container`, statics, …). |

Calls before the async import resolves are safe no-ops.

:::note[Re-mount semantics]
When any construction prop changes, the component destroys the instance and constructs a new one rather than diffing options. Same-`url` re-mounts are cheap because the core caches decoded peaks keyed by URL. For runtime track swaps without a re-mount, use `loadTrack()`. (A `:key` bound to the URL forces the same clean rebuild explicitly.)
:::

:::tip[DOM events]
Prefer the emits for lifecycle. If you need the raw `waveformplayer:*` [events](/player/events/) (e.g. `waveformplayer:request-seek` in external mode), attach via the instance: `player.value?.instance?.container.addEventListener(...)`.
:::

---

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

### Install

Peer deps: `@arraypress/waveform-bar` (`^1.3.1`), `@arraypress/waveform-player` (`^1.7.2`), Vue `^3.5.0`.

<Tabs syncKey="pkg">

<TabItem label="npm">

```bash
npm install @arraypress/waveform-bar-vue @arraypress/waveform-bar @arraypress/waveform-player vue
```

</TabItem>
<TabItem label="pnpm">

```bash
pnpm add @arraypress/waveform-bar-vue @arraypress/waveform-bar @arraypress/waveform-player vue
```

</TabItem>
<TabItem label="yarn">

```bash
yarn add @arraypress/waveform-bar-vue @arraypress/waveform-bar @arraypress/waveform-player vue
```

</TabItem>
<TabItem label="bun">

```bash
bun add @arraypress/waveform-bar-vue @arraypress/waveform-bar @arraypress/waveform-player vue
```

</TabItem>

</Tabs>

:::caution[Load order matters]
The bar has a hard runtime dependency on the player global. Import the **player before the bar** at your app entry, or `window.WaveformBar` will be undefined:

```ts

```
:::

### Mount the bar

```vue
<script setup lang="ts">

</script>

<template>
  <RouterView />
  <WaveformBar
    :config="{
      persist: true,
      continuous: true,
      showQueue: true,
      actions: {
        favorite: { endpoint: '/api/favorites' },
        cart: { endpoint: '/api/cart' },
      },
    }"
  />
</template>
```

On mount the component dynamically imports the core and calls `window.WaveformBar.init(config)`. When the config's structural shape changes it re-runs `init()` (idempotent — the library destroys and recreates internally); on unmount it calls `destroy()` so route changes and HMR reloads don't leak listeners.

| `<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 `style` fall through to the persist host `<div>` (base class `wb-host`).

:::note[Config change detection]
The config is compared by a `JSON.stringify` key, so passing a fresh object with the same shape every render is safe. Function endpoints (`actions.*.endpoint`) serialize to a placeholder, so if you pass a function endpoint, keep the whole `config` reference stable (hoist it or wrap it in a `computed`) to avoid re-init churn.
:::

#### `WaveformBarConfig`

Bar-vue 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'`), `shuffle` |
| UI toggles | `showQueue`, `showPrevNext`, `showRepeat`, `showShuffle`, `showVolume`, `showMute`, `showTime`, `showTrackLink`, `showMeta`, `maxMeta` (number) |
| Layout | `mode` (`'waveform' \| 'classic'`), `wide`, `position` (`'bottom' \| 'top'`), `collapsible` |
| 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? }`) |

`mode` defaults to `'waveform'` (the standard layout with a width-adjustable waveform). Set `mode: 'classic'` for the Spotify-style centre layout with a full-width seek bar. `showShuffle` adds a shuffle toggle to the transport cluster; `shuffle` starts with random queue advance on.

### Trigger a track

`<WaveformBarTrigger>` emits the `data-wb-*` attribute contract the bar scans for. It is polymorphic — a `<button>` by default — and any unlisted DOM attribute or native listener (`@click`, `data-testid`, `role`, …) falls through to the rendered element.

```vue
<script setup lang="ts">

</script>

<template>
  <!-- Default: a <button> with auto play/pause icons -->
  <WaveformBarTrigger
    url="/audio/track.mp3"
    id="track-42"
    title="Midnight Dreams"
    artist="The Wavelength"
    artwork="/img/cover.jpg"
  />

  <!-- Append to the queue with custom content -->
  <WaveformBarTrigger mode="queue" url="/audio/track.mp3" title="Midnight Dreams">
    + Queue
  </WaveformBarTrigger>

  <!-- Wrap a whole card as the trigger -->
  <WaveformBarTrigger as="div" :url="track.url" no-default-icons>
    <article class="product-card">…</article>
  </WaveformBarTrigger>
</template>
```

**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` | **Required** — 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 | Falls through to the element; auto-generated from `title` when absent. |
| default slot | — | — | Custom content; overrides the default icons. |

`class` is merged with the base class `wb-icon-swap`.

:::note[No callback props on the bar]
The bar deliberately exposes no emits and no imperative ref — an inline function each render would re-init the singleton. The core dispatches every state change as a bubbling `waveformbar:*` `CustomEvent` (`waveformbar:play`, `waveformbar:trackchange`, …); listen via `addEventListener` in your own effect. That stays framework-agnostic and avoids re-init churn.
:::

---

## Playlist — `<WaveformPlaylist>`

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

### Install

Peer deps: `@arraypress/waveform-playlist` (`^1.3.0`), `@arraypress/waveform-player` (`^1.8.0`), Vue `^3.5.0`.

<Tabs syncKey="pkg">

<TabItem label="npm">

```bash
npm install @arraypress/waveform-playlist-vue @arraypress/waveform-playlist @arraypress/waveform-player vue
```

</TabItem>
<TabItem label="pnpm">

```bash
pnpm add @arraypress/waveform-playlist-vue @arraypress/waveform-playlist @arraypress/waveform-player vue
```

</TabItem>
<TabItem label="yarn">

```bash
yarn add @arraypress/waveform-playlist-vue @arraypress/waveform-playlist @arraypress/waveform-player vue
```

</TabItem>
<TabItem label="bun">

```bash
bun add @arraypress/waveform-playlist-vue @arraypress/waveform-playlist @arraypress/waveform-player vue
```

</TabItem>

</Tabs>

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

```ts

```

### Quick start

```vue
<script setup lang="ts">

const 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' },
    ],
  },
];
</script>

<template>
  <WaveformPlaylist continuous waveform-style="bars" :tracks="tracks" />
</template>
```

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

### `tracks` (required)

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

### Props

`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. **Vue extras** — the required `tracks` (plus `class`, `style`, `id` falling 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`. |

Common pass-through player props include `waveformStyle`, `height`, `colorPreset`, `waveformColor`, `progressColor`, `showBPM`, `accessibleSeek`, and `autoplay` — see [Player → Options](/player/options/) for the complete list.

### Imperative API

Grab the instance with a template `ref` typed as `WaveformPlaylistExpose`:

```vue
<script setup lang="ts">

  WaveformPlaylist,
  type WaveformPlaylistExpose,
} from '@arraypress/waveform-playlist-vue';

const playlist = ref<WaveformPlaylistExpose>();
</script>

<template>
  <WaveformPlaylist ref="playlist" :tracks="tracks" continuous />
  <button @click="playlist?.previousTrack()">Prev</button>
  <button @click="playlist?.nextTrack()">Next</button>
  <button @click="playlist?.selectTrack(0)">First</button>
</template>
```

| 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`). |
| `instance` | `readonly WaveformPlaylist \| null` | Escape hatch for anything else. |

Calls before the async import resolves are no-ops.

:::note[No emits on the playlist]
The playlist owns the embedded player's callbacks internally to drive continuous playback and chapter tracking, so it exposes **no lifecycle emits** — forwarding them would yield events that never fire. Observe playback through the embedded player from `getPlayer()`.
:::

:::note[Re-mount semantics]
Changing `tracks` (compared by serialized value) or any construction-time option tears the playlist down and rebuilds it against the freshly rendered markup. The watcher uses `flush: 'post'` so the new track markup is in the DOM before the constructor re-parses it. For navigation without a re-mount, use the handle above.
:::

---

## Server-side rendering & Nuxt

All three components are SSR / Nuxt safe by construction: the host markup (and, for the playlist, the `[data-track]` children) renders on the server, and the browser-only core is dynamically `import()`ed inside `onMounted` — which runs **only on the client**, never during server render. The interactive UI hydrates in once the library loads.

No extra guarding is needed for the imperative path: keep instantiation inside `onMounted` (the wrappers already do), never at module top level. Reserve layout space with `style` (e.g. `:style="{ minHeight: '96px' }"`) to avoid a content shift before the waveform draws.

For Nuxt, you can render the components directly. To skip server rendering entirely and sidestep any hydration mismatch, wrap them in `<ClientOnly>`:

```vue
<template>
  <ClientOnly>
    <WaveformPlayer url="/audio/track.mp3" title="Midnight Drive" />
    <template #fallback>
      <div class="waveform-placeholder" style="min-height: 96px" />
    </template>
  </ClientOnly>
</template>

<script setup lang="ts">

</script>
```

<Aside type="tip" title="Importing on the server is safe">
Importing the wrapper module does not touch the DOM — the browser-only core is only pulled in (and constructed) client-side inside `onMounted`. If you reach for a browser-dependent core helper at request time, guard it with <code>import.meta.client</code> (Nuxt) or <code>typeof window !== 'undefined'</code>.
</Aside>

## Related

<CardGrid>

  <Card title="Player options" icon="setting">
    Full inherited option surface — [Player → Options](/player/options/).
  </Card>
  <Card title="Events" icon="rss">
    The `waveformplayer:*` DOM events — [Player → Events](/player/events/).
  </Card>
  <Card title="React" icon="seti:react">
    The same wrappers, typed for React — [React](/frameworks/react/).
  </Card>
  <Card title="Installation" icon="download">
    Bundler, CDN, and CSS setup — [Getting started](/getting-started/installation/).
  </Card>

</CardGrid>
