Skip to content

WordPress

WaveformPlayer is a single CSS file and a single self-initialising script, so it drops into any WordPress theme or plugin with wp_enqueue_style / wp_enqueue_script. Once the script is on the page it scans for [data-waveform-player] on DOMContentLoaded and upgrades every matching element — no inline JS required.

The recommended flow:

  1. Enqueue the stylesheet and script (from a CDN, or bundled in your theme/plugin).
  2. Emit the markup — a <div data-waveform-player …> — from post content, a shortcode, or a block.

The browser build is an IIFE that exposes window.WaveformPlayer, so it works as a plain classic script (no type="module" needed). Enqueue both files on wp_enqueue_scripts.

The fastest path — point WordPress at jsDelivr or unpkg. These use @latest for the current release; pin an exact version (@1.23.0) instead for reproducible builds.

// functions.php (theme) or your plugin's main file
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style(
'waveform-player',
'https://cdn.jsdelivr.net/npm/@arraypress/waveform-player@latest/dist/waveform-player.css',
array(),
'1.14.0'
);
wp_enqueue_script(
'waveform-player',
'https://cdn.jsdelivr.net/npm/@arraypress/waveform-player@latest/dist/waveform-player.min.js',
array(), // no dependencies
'1.14.0',
true // load in the footer
);
} );

To avoid a third-party CDN, copy the two files out of the package into your asset folder and enqueue them locally. Install the package to grab the dist/ files:

Terminal window
npm install @arraypress/waveform-player

Copy node_modules/@arraypress/waveform-player/dist/waveform-player.css and …/dist/waveform-player.min.js into your theme (e.g. assets/), then enqueue from there:

// In a theme — use get_theme_file_uri()
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style(
'waveform-player',
get_theme_file_uri( 'assets/waveform-player.css' ),
array(),
'1.14.0'
);
wp_enqueue_script(
'waveform-player',
get_theme_file_uri( 'assets/waveform-player.min.js' ),
array(),
'1.14.0',
true
);
} );
// In a plugin — use plugins_url() relative to the plugin file
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style(
'waveform-player',
plugins_url( 'assets/waveform-player.css', __FILE__ ),
array(),
'1.14.0'
);
wp_enqueue_script(
'waveform-player',
plugins_url( 'assets/waveform-player.min.js', __FILE__ ),
array(),
'1.14.0',
true
);
} );

The two routes above enqueue the browser build as a classic script, which self-initialises — right for a theme, where the page is yours.

A block is a different situation. Your JavaScript is bundled and runs inside a page built from post content you didn’t author, and in the editor inside an iframe you don’t own. Importing the package root there would scan that whole document and upgrade every [data-waveform-player] it found, including markup belonging to other plugins. Import the non-scanning entry point instead:

import WaveformPlayer from '@arraypress/waveform-player/no-autoinit';
// Build your own container, then mount exactly that one:
const container = document.createElement( 'div' );
container.setAttribute( 'data-waveform-player', '' );
container.setAttribute( 'data-url', url );
blockRoot.appendChild( container );
new WaveformPlayer( container );

Constructing the container yourself has a second benefit: the player only ever reads data-* attributes you set, so nothing an author typed into post content can reach the option surface.

A player is just a marker <div> carrying data-* attributes. The minimum is data-waveform-player plus an audio source:

<div
data-waveform-player
data-src="https://example.com/audio/track.mp3"
data-waveform-style="mirror"
data-title="Midnight Drive"
data-show-bpm="true"
></div>

There are three ways to get that markup into a WordPress page.

Option A — Custom HTML block / post content

Section titled “Option A — Custom HTML block / post content”

In the block editor, paste the <div> into a Custom HTML block. It will render and auto-initialise on the front end.

A shortcode is the most portable route: it works in the classic editor, the Shortcode block, widgets, and template calls via do_shortcode(), and it escapes every value server-side. This example maps a handful of shortcode attributes onto the player’s data-* contract and prints the <div>:

/**
* [waveform src="…" style="mirror" title="…" artist="…" height="64" show-bpm="false" peaks="…"]
*
* Renders a WaveformPlayer container. The enqueued script upgrades it on load.
*/
function wfp_waveform_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'src' => '', // audio URL (required) -> data-src
'style' => 'mirror', // bars|mirror|line|blocks|dots|seekbar
'title' => '', // -> data-title
'artist' => '', // -> data-artist
'height' => '64', // -> data-height
'show-bpm' => 'false', // -> data-show-bpm
'peaks' => '', // pre-computed peaks: .json URL, CSV, or JSON array -> data-waveform
),
$atts,
'waveform'
);
if ( empty( $atts['src'] ) ) {
return '';
}
// Build the attribute map; only emit what was provided.
$attributes = array(
'data-waveform-player' => '',
'data-src' => esc_url( $atts['src'] ),
'data-waveform-style' => sanitize_html_class( $atts['style'] ),
'data-height' => absint( $atts['height'] ),
'data-show-bpm' => ( 'true' === $atts['show-bpm'] ) ? 'true' : 'false',
);
if ( '' !== $atts['title'] ) {
$attributes['data-title'] = sanitize_text_field( $atts['title'] );
}
if ( '' !== $atts['artist'] ) {
$attributes['data-artist'] = sanitize_text_field( $atts['artist'] );
}
if ( '' !== $atts['peaks'] ) {
$attributes['data-waveform'] = $atts['peaks'];
}
$html = '';
foreach ( $attributes as $name => $value ) {
$html .= sprintf( ' %s="%s"', $name, esc_attr( $value ) );
}
return '<div' . $html . '></div>';
}
add_shortcode( 'waveform', 'wfp_waveform_shortcode' );

Authors then write:

[waveform src="https://example.com/audio/track.mp3" style="mirror" title="Midnight Drive" show-bpm="true"]

For a native block, register a dynamic block whose render_callback emits the same markup. The cleanest approach is to delegate to the shortcode function so there’s a single source of truth:

add_action( 'init', function () {
register_block_type( 'arraypress/waveform', array(
'api_version' => 3,
'attributes' => array(
'src' => array( 'type' => 'string', 'default' => '' ),
'style' => array( 'type' => 'string', 'default' => 'mirror' ),
'title' => array( 'type' => 'string', 'default' => '' ),
'showBpm' => array( 'type' => 'boolean', 'default' => false ),
),
'render_callback' => function ( $attributes ) {
return wfp_waveform_shortcode( array(
'src' => $attributes['src'] ?? '',
'style' => $attributes['style'] ?? 'mirror',
'title' => $attributes['title'] ?? '',
'show-bpm' => ! empty( $attributes['showBpm'] ) ? 'true' : 'false',
) );
},
) );
} );

Pair this with a block.json and an edit component for the editor UI (standard @wordpress/create-block scaffolding). The front-end output is identical to the shortcode, so the enqueued script upgrades it the same way.

Auto-init only fires once, on DOMContentLoaded. If markup arrives later — AJAX, infinite scroll, a block preview re-render, or a tabbed/accordion panel revealed after load — re-scan the DOM yourself. WaveformPlayer.init() is idempotent (it sets data-waveform-initialized="true" and skips anything already upgraded):

// After injecting new [data-waveform-player] markup into the page
window.WaveformPlayer.init();

By default the player downloads the audio and decodes it with Web Audio to build the waveform. For a media-heavy WordPress site, generate the peaks once and serve them as a sibling .json file, then pass it via data-waveform (the peaks shortcode attribute above). This skips the Web Audio decode entirely and draws instantly:

<div
data-waveform-player
data-src="https://example.com/audio/track.mp3"
data-waveform="https://example.com/audio/track.json"
></div>

The .json may be a bare peaks array or an object with embedded { peaks, markers }. See pre-computed peaks for generating the file (including WaveformPlayer.getPeaksUrl() to derive the sibling URL).

A quick reference for the attributes you’ll reach for most in shortcodes and blocks. The canonical key always wins over its alias.

Attribute Maps to Notes
data-src / data-url audio source data-url is canonical; data-src is the alias.
data-waveform-style / data-style visual style bars, mirror, line, blocks, dots, seekbar.
data-title / data-artist info block Title falls back to the filename if omitted.
data-artwork artwork image 40×40 thumbnail next to the title.
data-height canvas height (px) Default 64.
data-color-preset dark / light Omit for auto theme detection.
data-waveform-color / data-progress-color colours CSS colour, or a JSON array of gradient stops.
data-show-bpm BPM badge Boolean string "true". Pair with data-bpm to set a known value.
data-waveform pre-computed peaks .json URL, CSV, or JSON array — skips decoding.
data-markers cue markers JSON array of { time, label, color? }.

See the full data attributes reference for every key, and options for the JavaScript equivalents.

  • One global, classic script. The enqueued build defines window.WaveformPlayer, so it is a non-module script — wp_enqueue_script with the default args is correct. Don’t add type="module" via script_loader_tag.
  • Footer vs. head. Enqueue in the footer (true). The script attaches a DOMContentLoaded listener, so it upgrades players regardless of where it lands, but the footer keeps it out of the critical path.
  • Escaping is your job. WordPress doesn’t escape shortcode output — always run URLs through esc_url() and other values through esc_attr() / sanitize_text_field(), as in the example above.
  • Console prefix. Every log and error the library emits is prefixed [WaveformPlayer], which makes front-end debugging in WordPress straightforward.
  • Enqueuing in a theme vs. bundling in a block. An enqueued classic script scanning the page is the point — that’s how the markup in post content becomes players. Inside a block bundle it is a liability, because the document isn’t yours. Use /no-autoinit there. If you need to suppress the scan on a page you do control, data-waveform-autoinit="false" on <html> (via the language_attributes filter) does it without changing your import.

Installation

All install paths — CDN, bundler, and the data-attribute auto-init contract. Getting started

Data attributes

The complete declarative API surface. Reference

Options

The JavaScript constructor options behind every attribute. Options

Pre-computed peaks

Generate and serve .json peaks to skip decoding. Peaks