# Shopify

> Add WaveformPlayer to a Shopify theme.

WaveformPlayer is a single CSS file and a single browser-global script — no build step, no bundler. That makes it a natural fit for Shopify themes, where you drop the assets into your theme, then render players declaratively with `data-*` attributes generated by Liquid (a product audio file, a metafield, a section setting, and so on).

This page covers both hosting options (theme `assets/` or CDN), the Liquid markup contract, a reusable snippet, a full section with schema settings, and the Theme Editor lifecycle hooks you need so players keep working when merchants add or remove sections live.

## How it loads

The IIFE build (`dist/waveform-player.min.js`) exposes a `window.WaveformPlayer` global and **auto-initialises on `DOMContentLoaded`**: it scans the DOM for any element carrying `data-waveform-player` and instantiates a player for it. You never have to write JavaScript for the common case — emit the right markup from Liquid and the script does the rest.

`WaveformPlayer.init()` is idempotent (it marks each element with `data-waveform-initialized="true"` and skips already-initialised nodes), which matters for Shopify's Theme Editor — see [Theme Editor lifecycle](#theme-editor-lifecycle).

## 1. Add the assets

You have two options. Self-hosting in the theme's `assets/` folder is recommended for production (one origin, no third-party dependency, served from Shopify's CDN). A public CDN is fine for a quick prototype.

### Option A — self-host in `assets/`

1. Get the two dist files. Install the package and copy them out, or download them from a CDN.

<Tabs syncKey="pkg">

<TabItem label="npm">

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

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

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

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

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

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

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

</TabItem>

</Tabs>

   You need exactly two files from `node_modules/@arraypress/waveform-player/dist/`:

   - `waveform-player.min.js`
   - `waveform-player.css`

2. Upload both to your theme's **Assets** (Online Store → Themes → ⋯ → Edit code → Assets → Add a new asset), or via the Shopify CLI / GitHub integration. Shopify's `assets/` folder is **flat** — no subfolders — so they live as `assets/waveform-player.min.js` and `assets/waveform-player.css`.

3. Reference them in `layout/theme.liquid`, inside `<head>` for the stylesheet and before `</body>` for the script.

   ```liquid
   {{ 'waveform-player.css' | asset_url | stylesheet_tag }}

   <script src="{{ 'waveform-player.min.js' | asset_url }}" defer></script>
   ```

The `asset_url` filter rewrites the filename to the theme's CDN URL, and `stylesheet_tag` emits a proper `<link rel="stylesheet">`. `defer` lets the script load without blocking render; auto-init still fires on `DOMContentLoaded`.

:::tip[Load only where needed]
If audio appears on a handful of templates, skip `theme.liquid` and load the assets from the section that needs them instead (see [section example](#full-section)). Shopify dedupes identical asset URLs, so referencing the same file from several sections on one page is safe.
:::

### Option B — CDN

For prototypes or themes where you would rather not manage asset files, point at jsDelivr or unpkg. Pin a major version (`@1`) so you get patches without surprise breakage.

```liquid
{# jsDelivr #}
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/@arraypress/waveform-player@1/dist/waveform-player.css">
<script
  src="https://cdn.jsdelivr.net/npm/@arraypress/waveform-player@1/dist/waveform-player.min.js"
  defer></script>
```

```liquid
{# unpkg (the package's "unpkg" entry resolves to the min build) #}
<script src="https://unpkg.com/@arraypress/waveform-player@1" defer></script>
```

## 2. The markup contract

A player is any element with `data-waveform-player` plus a source. Every constructor option has a `data-*` equivalent, parsed by the library at init — so a fully-configured player is pure markup. The minimum is a source URL:

```liquid
<div
  data-waveform-player
  data-src="{{ 'demo-track.mp3' | asset_url }}"></div>
```

`data-src` is an alias for `data-url`; if both are present, `data-url` (canonical) wins. The full list of attributes is on the [Data attributes](/player/data-attributes/) page; the most useful ones for theme work:

| Attribute | Maps to | Notes |
| --- | --- | --- |
| `data-src` / `data-url` | `url` | Audio file URL. `data-url` wins if both set. |
| `data-title` | `title` | Falls back to the URL filename if omitted. |
| `data-artist` | `artist` | Second info line (e.g. artist). |
| `data-artwork` | `artwork` | 40×40 thumbnail + Media Session art. |
| `data-waveform-style` / `data-style` | `waveformStyle` | `bars` · `mirror` · `line` · `blocks` · `dots` · `seekbar`. |
| `data-height` | `height` | Waveform height in px (default `64`). |
| `data-waveform-color` | `waveformColor` | CSS colour, or a JSON array of gradient stops. |
| `data-progress-color` | `progressColor` | Played-through colour (or JSON gradient array). |
| `data-color-preset` | `colorPreset` | `dark` / `light`; omit to auto-detect the theme. |
| `data-waveform` | `waveform` | Pre-computed peaks (CSV / JSON array / `.json` URL) to skip Web Audio decode. |
| `data-show-bpm` | `showBPM` | Boolean string `"true"`. |
| `data-bpm` | `bpm` | Known BPM; wins over auto-detection. |
| `data-autoplay` | `autoplay` | Boolean string `"true"`. |
| `data-layout` | `layout` | `default` or `preview`. |

:::note[Booleans are strings]
Liquid renders everything as text, which is exactly what the parser expects. Booleans must be the literal string `"true"` (e.g. `data-show-bpm="true"`). Any other value — including an empty attribute — is treated as not set.
:::

:::caution[Escape dynamic strings]
When you interpolate merchant- or customer-controlled text (a title, artist, or a metafield), pass it through Liquid's `escape` filter so quotes and angle brackets can't break out of the attribute: `data-title="{{ block.settings.title | escape }}"`.
:::

## 3. Drive it from Liquid

The source is usually not a hard-coded asset. Common Shopify patterns:

```liquid
{# A) A file uploaded to Settings → Files, referenced by name #}
<div
  data-waveform-player
  data-src="{{ 'single-preview.mp3' | file_url }}"
  data-title="Single preview"></div>

{# B) A product metafield holding the audio file (file_reference type) #}
{%- assign audio = product.metafields.custom.audio_file -%}
{%- if audio != blank -%}
  <div
    data-waveform-player
    data-src="{{ audio | file_url }}"
    data-title="{{ product.title | escape }}"
    data-artwork="{{ product.featured_image | image_url: width: 80 }}"
    data-waveform-style="mirror"
    data-show-bpm="true"></div>
{%- endif -%}

{# C) A URL/text metafield (just output the value) #}
<div
  data-waveform-player
  data-src="{{ product.metafields.custom.preview_url }}"
  data-title="{{ product.title | escape }}"></div>
```

:::tip[Skip the decode with pre-computed peaks]
Decoding audio in the browser to build the waveform costs a download and CPU. If you precompute peaks and upload a sibling `.json` file (same base name as the audio), point `data-waveform` at it to skip Web Audio entirely:

```liquid
<div
  data-waveform-player
  data-src="{{ 'track.mp3' | file_url }}"
  data-waveform="{{ 'track.json' | file_url }}"></div>
```

The JSON may be a bare peaks array or an object with `peaks` and embedded `markers`. See [Waveform data](/player/waveform-data/).
:::

### Looping a collection

Render a player per product in a collection (or per line item in a cart), guarding on the metafield so products without audio are skipped:

```liquid
<ul class="audio-grid">
  {%- for product in collection.products -%}
    {%- assign audio = product.metafields.custom.audio_file -%}
    {%- if audio != blank -%}
      <li>
        <div
          data-waveform-player
          data-src="{{ audio | file_url }}"
          data-title="{{ product.title | escape }}"
          data-artist="{{ product.vendor | escape }}"
          data-artwork="{{ product.featured_image | image_url: width: 80 }}"
          data-waveform-style="bars"
          data-layout="preview"></div>
      </li>
    {%- endif -%}
  {%- endfor -%}
</ul>
```

By default each player pauses every other instance when it starts (`singlePlay`, on by default), so a grid behaves like one playlist — only one track plays at a time. Set `data-single-play="false"` on a player to opt it out.

## Reusable snippet

Extract the markup into `snippets/waveform-player.liquid` so templates stay clean. Render it with named parameters:

```liquid
{% comment %} snippets/waveform-player.liquid
  Renders a WaveformPlayer. Required: url. Optional: title, artist,
  artwork, style, peaks (a .json peaks URL), bpm.
{% endcomment %}
{%- if url != blank -%}
  <div
    data-waveform-player
    data-src="{{ url }}"
    {% if title != blank %}data-title="{{ title | escape }}"{% endif %}
    {% if artist != blank %}data-artist="{{ artist | escape }}"{% endif %}
    {% if artwork != blank %}data-artwork="{{ artwork }}"{% endif %}
    {% if peaks != blank %}data-waveform="{{ peaks }}"{% endif %}
    {% if bpm != blank %}data-show-bpm="true" data-bpm="{{ bpm }}"{% endif %}
    data-waveform-style="{{ style | default: 'mirror' }}"></div>
{%- endif -%}
```

Call it from a product template:

```liquid
{% render 'waveform-player',
   url: product.metafields.custom.audio_file | file_url,
   title: product.title,
   artist: product.vendor,
   artwork: product.featured_image | image_url: width: 80,
   style: 'mirror' %}
```

## Full section

A self-contained section with schema settings lets merchants drop a player onto any page from the Theme Editor and configure it visually. Save as `sections/audio-player.liquid`.

```liquid
{%- comment -%} sections/audio-player.liquid {%- endcomment -%}
{{ 'waveform-player.css' | asset_url | stylesheet_tag }}

<div class="audio-player-section" style="max-width: {{ section.settings.max_width }}px">
  {%- assign source = section.settings.audio_file | default: section.settings.audio_url -%}
  {%- if source != blank -%}
    <div
      data-waveform-player
      data-src="{{ source | file_url }}"
      data-title="{{ section.settings.title | escape }}"
      data-artist="{{ section.settings.artist | escape }}"
      data-waveform-style="{{ section.settings.style }}"
      data-color-preset="{{ section.settings.preset }}"
      data-height="{{ section.settings.height }}"
      {% if section.settings.waveform_color != blank %}
        data-waveform-color="{{ section.settings.waveform_color }}"{% endif %}
      {% if section.settings.show_bpm %}data-show-bpm="true"{% endif %}></div>
  {%- else -%}
    <p>{{ 'Add an audio file in the section settings.' }}</p>
  {%- endif -%}
</div>

<script src="{{ 'waveform-player.min.js' | asset_url }}" defer></script>

{% schema %}
{
  "name": "Audio player",
  "settings": [
    { "type": "text", "id": "title", "label": "Title" },
    { "type": "text", "id": "artist", "label": "Artist" },
    { "type": "url", "id": "audio_url", "label": "Audio URL" },
    {
      "type": "select", "id": "style", "label": "Waveform style", "default": "mirror",
      "options": [
        { "value": "bars", "label": "Bars" },
        { "value": "mirror", "label": "Mirror" },
        { "value": "line", "label": "Line" },
        { "value": "blocks", "label": "Blocks" },
        { "value": "dots", "label": "Dots" },
        { "value": "seekbar", "label": "Seekbar" }
      ]
    },
    {
      "type": "select", "id": "preset", "label": "Colour preset", "default": "dark",
      "options": [
        { "value": "dark", "label": "Dark" },
        { "value": "light", "label": "Light" }
      ]
    },
    { "type": "color", "id": "waveform_color", "label": "Waveform colour" },
    { "type": "range", "id": "height", "label": "Height", "min": 40, "max": 160, "step": 8, "unit": "px", "default": 64 },
    { "type": "range", "id": "max_width", "label": "Max width", "min": 320, "max": 960, "step": 20, "unit": "px", "default": 640 },
    { "type": "checkbox", "id": "show_bpm", "label": "Show BPM", "default": false }
  ],
  "presets": [{ "name": "Audio player" }]
}
{% endschema %}
```

The Theme Editor's `color` setting returns a CSS colour string, which maps straight onto `data-waveform-color`. The `url` setting accepts file picks and external URLs; `file_url` returns it unchanged for absolute URLs, so it's safe to apply in both cases.

:::note[`audio_file` isn't a Shopify setting type]
The `| default: section.settings.audio_url` above is there so you can later swap in a metafield or file-picker block as the primary source while keeping the URL setting as a fallback. There is no native "audio file" picker in section schema — use a `url` setting, a file metafield, or **Settings → Files** uploads.
:::

## Theme Editor lifecycle

In the Theme Editor, merchants add, remove, reorder, and re-render sections **without a full page reload**. Auto-init only runs once (on `DOMContentLoaded`), so a section injected after load won't be picked up — and a removed section leaves a player instance behind. Wire the four Shopify theme events to keep players in sync. Put this once in `theme.liquid` (or in your section, guarded so it only binds once):

```liquid
<script>
  document.addEventListener('shopify:section:load', function (event) {
    // A section was (re)rendered — init any new players inside it.
    // WaveformPlayer.init() is idempotent, so already-init'd nodes are skipped.
    if (window.WaveformPlayer) window.WaveformPlayer.init();
  });

  document.addEventListener('shopify:section:unload', function (event) {
    // Tear down players in the removed section to free audio + listeners.
    if (!window.WaveformPlayer) return;
    event.target
      .querySelectorAll('[data-waveform-player]')
      .forEach(function (el) {
        var player = window.WaveformPlayer.getInstance(el);
        if (player) player.destroy();
      });
  });
</script>
```

| Event | What to do |
| --- | --- |
| `shopify:section:load` | Call `WaveformPlayer.init()` to instantiate newly-rendered players (idempotent — safe to call repeatedly). |
| `shopify:section:unload` | `getInstance(el)` then `destroy()` each player in `event.target` to release the `<audio>`, listeners, and observers. |
| `shopify:block:select` / `shopify:block:deselect` | Optional: pause or highlight when a merchant selects a block. |

`destroy()` emits `waveformplayer:destroy`, pauses playback, aborts all listeners, disconnects the `ResizeObserver`, and empties the container — so removing a section won't leak audio or leave a track playing.

<Aside type="caution" title="Relocating a player">
Canvas redraw is driven by a `ResizeObserver` on the canvas's parent. If your theme JavaScript moves a player element in the DOM (e.g. into a drawer or modal) rather than re-rendering it, the observer may not fire — call `WaveformPlayer.getInstance(el).resizeCanvas()` after the move so the waveform repaints at its new size.
</Aside>

## Going beyond markup

For interactivity — a sticky bottom bar that follows the customer across the store, "add to playlist" buttons, or syncing a player with a custom control — drop down to the JavaScript API via the `window.WaveformPlayer` global:

```liquid
<script>
  document.addEventListener('DOMContentLoaded', function () {
    var el = document.querySelector('[data-waveform-player]');
    var player = window.WaveformPlayer.getInstance(el);

    player.container.addEventListener('waveformplayer:play', function (e) {
      // e.detail = { player, url } — fire analytics, etc.
    });
  });
</script>
```

See:

<CardGrid>

  <Card title="Options" icon="setting">
    Every constructor option, in full. [Options →](/player/options/)
  </Card>
  <Card title="Data attributes" icon="document">
    The complete `data-*` contract. [Data attributes →](/player/data-attributes/)
  </Card>
  <Card title="Methods & events" icon="document">
    The runtime API and event map. [API →](/player/methods/)
  </Card>
  <Card title="Waveform data" icon="random">
    Pre-computed peaks and `.json` sidecars. [Waveform data →](/player/waveform-data/)
  </Card>

</CardGrid>

## Checklist

1. `waveform-player.css` and `waveform-player.min.js` uploaded to `assets/` (or a pinned CDN URL).
2. Stylesheet linked in `<head>`, script loaded with `defer` before `</body>`.
3. Players rendered with `data-waveform-player` + `data-src` from Liquid.
4. Dynamic strings escaped with `| escape`; booleans emitted as the string `"true"`.
5. `shopify:section:load` / `shopify:section:unload` wired so the Theme Editor stays in sync.
