Skip to content

Styling & theming

The player ships a complete, themeable look out of the box and gives you three layers of control on top of it:

  1. Presets — a dark / light token set, chosen automatically from the surrounding page (and re-detected live when the page flips).
  2. Explicit colors — per-instance overrides for any individual token, including vertical gradients on the waveform.
  3. CSS custom properties — structural knobs (--wfp-btn-size, --wfp-accent, spacing) that live in the stylesheet so you can re-tint or re-scale without touching JavaScript.

Every player resolves to one of two built-in presets. Each preset sets the two waveform color tokens as JS options, deliberately translucent black/white so they sit on any host background:

Token (option) dark light
waveformColor rgba(255, 255, 255, 0.3) rgba(0, 0, 0, 0.2)
progressColor rgba(255, 255, 255, 0.9) rgba(0, 0, 0, 0.8)

The DOM chrome (button, title, meta text) is themed via CSS variables — --wfp-button-color, --wfp-text-color, --wfp-text-secondary-color — not JS options; override them in your own CSS.

Select a preset with colorPreset ('dark' | 'light' | null). The default, null, triggers auto-detection.

const player = new WaveformPlayer('#player', {
url: '/audio/track.mp3',
colorPreset: 'dark', // force dark; disables auto re-detection
});
<!-- Declarative equivalent -->
<div data-waveform-player data-src="/audio/track.mp3" data-color-preset="dark"></div>

When colorPreset is null (or an unrecognized value), the player calls detectColorScheme() and picks the matching preset. Resolution order, first match wins:

  1. Explicit theme hints on <html> / <body> — a class of dark / light, dark-mode / light-mode, or theme-dark / theme-light; or a data-theme / data-color-scheme attribute set to dark or light.

  2. Computed <body> background brightness — perceived brightness > 128 resolves to light, < 128 to dark. Exactly 128 or an unparseable color is ambiguous and falls through.

  3. OS preference — the prefers-color-scheme media query.

  4. Fallback'dark' (most audio players are dark).

This means a player dropped into a Tailwind dark-class page, a data-theme="light" page, or a page that simply has a dark <body> background will theme itself correctly with no configuration.

The player adapts to runtime light/dark switches, not just the value present at load. A single shared, event-driven watcher (a MutationObserver on <html>/<body> watching class, data-theme, data-color-scheme, and style, plus a prefers-color-scheme matchMedia listener) calls refreshTheme() on every live instance whenever the page theme changes.

The player below is auto-themed — flip this site’s light/dark toggle (top of the page) and watch it re-colour live:

You can also trigger it yourself:

// Re-detect the page theme and re-apply auto colors + redraw.
player.refreshTheme();

refreshTheme() only re-applies tokens the user left unset — the keys that originally inherited from the preset. It is a no-op when the player opted out of auto-theming, which happens if either:

  • colorPreset is set to an explicit valid value ('dark' or 'light'), or
  • the relevant color was hand-set via an option / data-* attribute.

The two waveform tokens have matching options. Set either to override the preset for that token only; leave it null to inherit.

Option Type Controls data-* attribute
waveformColor string | string[] | null Unplayed waveform fill data-waveform-color
progressColor string | string[] | null Played-through fill data-progress-color
new WaveformPlayer('#player', {
url: '/audio/track.mp3',
waveformColor: '#3f3f46',
progressColor: '#f97316',
});

waveformColor and progressColor accept an array of stops instead of a single string. The stops form a vertical gradient, top → bottom. A single-element array collapses to one flat color.

new WaveformPlayer('#player', {
url: '/audio/track.mp3',
waveformColor: ['#fafafa', '#71717a'], // light at top, fading down
progressColor: ['#f97316', '#b91c1c'],
});

In markup, pass a JSON array string:

<div
data-waveform-player
data-src="/audio/track.mp3"
data-waveform-color='["#fafafa", "#71717a"]'
data-progress-color='["#f97316", "#b91c1c"]'
></div>

For look-and-feel that the stylesheet owns, override CSS variables rather than passing options. The two you will reach for most:

Variable Default Effect
--wfp-btn-size 36px Scales both play-button styles — box and glyph — proportionally.
--wfp-accent #71717a Keyboard focus-ring outline + active-state accent (monochrome default).
--wfp-button-color preset Play-button border + icon color.
--wfp-text-color preset Title color.
--wfp-text-secondary-color preset Artist / time / BPM color.
--waveform-line-height 1.4 Root line-height of the player.
--waveform-body-gap 8px Vertical gap between the track row and the info block.
--waveform-track-gap 12px Horizontal gap between the play button and the waveform.

Set them on .waveform-player (or any ancestor) — and this is also where you’d add a visible card background/border, since the root ships transparent:

.waveform-player {
--wfp-accent: #f97316; /* brand-tinted focus ring */
--waveform-track-gap: 16px; /* more breathing room */
/* Optional visible surface */
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 16px;
}

The full set of class hooks (.waveform-btn, .waveform-container, .waveform-title, .waveform-meta, …) is available on the rendered DOM if you need finer-grained targeting.

buttonStyle switches the play/pause control between two looks:

Value Result Class added
'circle' Bordered circle (default).
'minimal' Bare play/pause glyph, no circle — the sample-pack grid look. .waveform-btn-minimal

buttonSize drives the --wfp-btn-size CSS variable, and every button dimension derives from it, so a single value scales the box and glyph together:

  • null (default) — the stylesheet default of 36px.
  • a number — treated as pixels (e.g. 4848px).
  • a string — used verbatim (e.g. '4rem').

Internally the glyph tracks the box: the circle’s SVG is 0.45 × the size; in minimal mode the box is 1.1 × and the glyph 0.94 ×. You never set those — just set the one knob.

new WaveformPlayer('#player', {
url: '/audio/track.mp3',
buttonStyle: 'minimal',
buttonSize: 48, // px
});

The button’s vertical alignment is a separate knob, buttonAlign ('auto' | 'top' | 'center' | 'bottom'); 'auto' resolves to bottom for the bars style and center for everything else, surfaced as .waveform-align-* on .waveform-track.

A minimal button at 4rem, live:

layout selects the overall composition:

Value Result
'default' Play button + waveform with a left-aligned info row (artwork / title / artist / meta) below.
'preview' Compact: the title is centered under the waveform and the meta row (time / speed / BPM) is trimmed. Adds .waveform-layout-preview to the root — ideal for sample-pack previews and dense grids.
new WaveformPlayer('#player', {
url: '/audio/loop.mp3',
layout: 'preview',
buttonStyle: 'minimal',
waveformStyle: 'bars',
});

The same options, live — note the centered title and trimmed meta row:

The visual style is chosen with waveformStyle'bars', 'mirror' (default), 'line', 'blocks', 'dots', or 'seekbar'. See Waveform styles for what each looks like; this section covers the geometry that tunes them.

Three options size the bars:

Option Default Effect
barWidth 2 Bar width in px.
barSpacing 0 Gap between bars in px.
barRadius 1 Rounded bar-cap radius in px (0 = square). Applies to bars/mirror only.

When you do not set barWidth / barSpacing (neither the option nor the data-* attribute), each style seeds its own natural geometry so it renders at sensible proportions:

waveformStyle barWidth barSpacing
bars 3 1
mirror 2 2
line 2 0
blocks 4 2
dots 3 3
seekbar 1 0

The moment you set either value explicitly, that style default is skipped for the value you set and your value is used as-is.

new WaveformPlayer('#player', {
url: '/audio/track.mp3',
waveformStyle: 'bars',
barWidth: 4, // overrides the bars default of 3
barSpacing: 2, // overrides the bars default of 1
barRadius: 2, // soft caps
});

For the complete option surface, data-* contract, and event map, see Options & data attributes.