Options
Every constructor option, in full. Options →
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.
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.
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.
assets/ npm install @arraypress/waveform-player pnpm add @arraypress/waveform-player yarn add @arraypress/waveform-player bun add @arraypress/waveform-playerYou need exactly two files from node_modules/@arraypress/waveform-player/dist/:
waveform-player.min.jswaveform-player.cssUpload 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.
Reference them in layout/theme.liquid, inside <head> for the stylesheet and before </body> for the script.
{{ '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.
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.
{# 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>{# unpkg (the package's "unpkg" entry resolves to the min build) #}<script src="https://unpkg.com/@arraypress/waveform-player@1" defer></script>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:
<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 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. |
The source is usually not a hard-coded asset. Common Shopify patterns:
{# 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>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:
<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.
Extract the markup into snippets/waveform-player.liquid so templates stay clean. Render it with named parameters:
{% 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:
{% 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' %}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.
{%- 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.
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):
<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.
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:
<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:
Options
Every constructor option, in full. Options →
Data attributes
The complete data-* contract. Data attributes →
Methods & events
The runtime API and event map. API →
Waveform data
Pre-computed peaks and .json sidecars. Waveform data →
waveform-player.css and waveform-player.min.js uploaded to assets/ (or a pinned CDN URL).<head>, script loaded with defer before </body>.data-waveform-player + data-src from Liquid.| escape; booleans emitted as the string "true".shopify:section:load / shopify:section:unload wired so the Theme Editor stays in sync.