Skip to content

Accessibility

Every player is keyboard-operable and screen-reader-aware out of the box. The waveform is exposed as a native role="slider", the play button and cue markers carry accessible names, the optional playback-speed menu announces its open state (aria-expanded) and closes on Escape, focus is visible, animation honours prefers-reduced-motion, and self-mode players register system Media Session controls. Nothing on this page requires configuration — it is on by default.

A player exposes two distinct keyboard targets, each with its own keys:

Element CSS class In tab order? Handles
The seek slider .waveform-container Yes (tabindex="0") Arrow / Page / Home / End seeking — both audio modes
The player root .waveform-player Only after a click (tabindex flips -10) Space, digits 09, volume, mute — self mode

The slider is reachable with Tab. Clicking an interactive control keeps focus on it — the play button and cue markers activate without moving focus, and clicking the waveform focuses the slider. Clicking a non-interactive area (the title, the info row, surrounding padding) focuses the root, which also pulls focus off sibling players so transport keys only ever drive one instance.

Focus the waveform with Tab, then:

Key Action
/ Seek forward 5 s
/ Seek back 5 s
Page Up Seek forward 10 s
Page Down Seek back 10 s
Home Jump to start (0 s)
End Jump to the end

These work in both audio modes. In self mode they call seekTo() directly; in external mode they dispatch a cancelable waveformplayer:request-seek event (the controller may preventDefault() to veto it; otherwise the local overlay advances optimistically). Seeking is a no-op until a duration is known.

Click a non-interactive area of the player to focus its root, then:

Key Action Mode
Space Toggle play / pause Both
09 Seek to that tenth of the track (0 = start, 5 = halfway, 9 = 90%) Self
Seek forward 5 s Self
Seek back 5 s Self
Volume +0.1 Self
Volume −0.1 Self
m / M Toggle mute Self

Space works in external mode too — togglePlay() routes through the request-play / request-pause events so the controller decides what happens. The remaining keys depend on an owned <audio> element, so they are inert in external mode (the external controller owns volume, mute and fine seeking).

When accessibleSeek is true (the default), .waveform-container is upgraded to a standard ARIA slider. The library sets and continuously updates these attributes:

Attribute Value
role "slider"
tabindex "0"
aria-valuemin "0"
aria-valuemax Duration in seconds, rounded
aria-valuenow Current time in seconds, rounded
aria-valuetext Human-readable position, e.g. "1:23 of 3:45"
aria-label The slider’s accessible name (see below)
aria-busy "true" while the track is loading, "false" once ready

aria-valuenow / aria-valuetext refresh on every progress tick in both modes, so a screen reader always announces the live position. aria-busy lets assistive tech know a track is still decoding.

Set accessibleSeek: false to opt out entirely — the slider role, tab stop and keyboard seeking are all removed (click-to-seek on the canvas still works).

Disable the keyboard slider
new WaveformPlayer('#player', {
url: '/audio/track.mp3',
accessibleSeek: false,
});

seekLabel sets the slider’s accessible name. When unset it falls back to the track title, and finally to the literal string "Seek". The name is re-applied whenever the track changes, so a loadTrack() swap keeps the label in sync.

Explicit slider label
new WaveformPlayer('#player', {
url: '/audio/interview.mp3',
title: 'Episode 42',
seekLabel: 'Seek within Episode 42',
});
Declarative equivalent
<div
data-waveform-player
data-src="/audio/interview.mp3"
data-title="Episode 42"
data-seek-label="Seek within Episode 42"
></div>

As playback moves, the slider’s aria-valuetext is announced as the current position out of the total — by default "0:30 of 2:00". seekValueText templates that string so the connective word is translatable: %1$s is the current time and %2$s the total (both formatted M:SS, still formatted by the library). Sequential %s works too, and positional args may be reordered for languages that need the total first.

Localized value text
new WaveformPlayer('#player', {
url: '/audio/track.mp3',
seekValueText: '%1$s de %2$s', // → "0:30 de 2:00"
});

The default '%1$s of %2$s' reproduces the previous hardcoded output exactly, so existing players are unchanged.

Beyond the slider, the rendered DOM is labelled throughout. Every string is an option (each also settable via data-*), so a non-English UI can translate all of it:

Element CSS class ARIA Localize with
Play / pause button .waveform-btn aria-label playPauseLabel (default "Play/Pause")
Speed trigger + menu .speed-btn / .speed-menu aria-label speedLabel (default "Playback speed")
Artwork image .waveform-artwork alt artworkAlt (default "Album artwork")
Cue markers .waveform-marker aria-label each marker’s label
Error overlay .waveform-error role="alert" errorText (default "Unable to load audio")

Marker labels come straight from your markers data, so give each cue a meaningful label. The error overlay text comes from errorText (escaped) and is announced live via role="alert".

All screen-reader / lock-screen text is exposed as options so you can translate it without touching the DOM. Combined with seekLabel and seekValueText, these cover every string the library renders:

Option Default Where it is announced
playPauseLabel 'Play/Pause' Play button aria-label
speedLabel 'Playback speed' Speed button + menu aria-label
artworkAlt 'Album artwork' Artwork image alt
errorText 'Unable to load audio' role="alert" error overlay
seekValueText '%1$s of %2$s' Seek slider spoken aria-valuetext
unknownTrackText 'Unknown Track' Media Session title when no title is set
Spanish UI
new WaveformPlayer('#player', {
url: '/audio/track.mp3',
seekLabel: 'Buscar',
seekValueText: '%1$s de %2$s',
playPauseLabel: 'Reproducir/Pausar',
speedLabel: 'Velocidad',
artworkAlt: 'Portada del álbum',
errorText: 'No se pudo cargar el audio',
unknownTrackText: 'Pista desconocida',
});
Declarative equivalent
<div
data-waveform-player
data-src="/audio/track.mp3"
data-seek-label="Buscar"
data-seek-value-text="%1$s de %2$s"
data-play-pause-label="Reproducir/Pausar"
data-speed-label="Velocidad"
data-artwork-alt="Portada del álbum"
data-error-text="No se pudo cargar el audio"
data-unknown-track-text="Pista desconocida"
></div>

The library ships no built-in translations — that keeps the core lean and lets your own i18n pipeline (WordPress text domains, react-i18next, etc.) own the strings. But if you just want a drop-in, here are ready-made objects for common languages. Spread one into your options (or emit the matching data-* attributes).

{
playPauseLabel: 'Lecture/Pause',
speedLabel: 'Vitesse de lecture',
artworkAlt: 'Pochette de l’album',
errorText: 'Impossible de charger l’audio',
seekValueText: '%1$s sur %2$s',
unknownTrackText: 'Piste inconnue',
}

Focus is never suppressed for keyboard users. The stylesheet ships :focus-visible rings on each interactive surface, so a mouse click stays quiet while Tab navigation shows a clear indicator:

Selector Ring
.waveform-container:focus-visible 2px solid currentColor (the seek slider)
.waveform-btn:focus-visible 2px solid currentColor, 2px offset
.waveform-marker:focus-visible 2px solid currentColor, 1px offset
.waveform-player:focus-visible 1px solid var(--wfp-accent), 1px offset

The ring colour is driven by --wfp-accent (and currentColor for the slider/button/markers), so it tracks your theme automatically. Override it per player or globally:

Custom focus accent
.waveform-player {
--wfp-accent: #6366f1;
}

The stylesheet respects the OS “reduce motion” setting. Under @media (prefers-reduced-motion: reduce), every transition and animation inside .waveform-player is neutralised:

Shipped in waveform-player.css
@media (prefers-reduced-motion: reduce) {
.waveform-player *,
.waveform-player *::before,
.waveform-player *::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}

State still changes instantly (play icon, progress fill, markers) — only the easing and the loading spinner’s motion are removed. No configuration is required; this applies as soon as you import the stylesheet.

In self mode, a player registers with the Media Session API so the OS lock screen, notification shade, keyboard media keys, headset buttons and browser media hub can control and label playback. This is gated on enableMediaSession (default true) and on the browser actually supporting navigator.mediaSession.

The player publishes metadata from your existing options:

Media Session field Source option
title title (falls back to unknownTrackText, default "Unknown Track")
artist artist
album album
artwork artwork (advertised at 512x512)

And wires these action handlers:

Action Behaviour
play / pause play() / pause()
seekbackward Seek back 10 s
seekforward Seek forward 10 s
seekto Seek to the requested absolute time
Rich system controls
new WaveformPlayer('#player', {
url: '/audio/track.mp3',
title: 'Midnight City',
artist: 'M83',
album: 'Hurry Up, We\u2019re Dreaming',
artwork: '/art/m83.jpg',
enableMediaSession: true, // default
});

External mode keeps full accessibility even without an owned <audio> element:

  • The seek slider still renders with role="slider" and full ARIA values, driven by the external clock via setProgress().
  • Arrow / Page / Home / End seeking and Space still work — they dispatch cancelable waveformplayer:request-seek / request-play / request-pause events for the controller to honour.
  • aria-valuenow / aria-valuetext update from setProgress(currentTime, duration), so announcements stay accurate against the external source of truth.

What does not apply in external mode: digit-key seeking, arrow-key volume, m mute, and Media Session — those need a player-owned <audio> element.

Option Type Default Purpose
accessibleSeek boolean true Expose the waveform as a keyboard-seekable role="slider".
seekLabel string | null null Slider accessible name; falls back to title, then "Seek".
seekValueText string | null null Spoken aria-valuetext template (%1$s current, %2$s total); falls back to '%1$s of %2$s'.
playPauseLabel string 'Play/Pause' Play button aria-label.
speedLabel string 'Playback speed' Speed button + menu aria-label.
enableMediaSession boolean true Register system Media Session controls (self mode only).
title string | null null Media Session title + slider name fallback.
unknownTrackText string 'Unknown Track' Media Session title fallback when no title is set.
artist string | null null Media Session artist.
album string '' Media Session album metadata.
artwork string | null null Media Session artwork (and the 40×40 info image).
artworkAlt string 'Album artwork' Artwork image alt text.
errorText string 'Unable to load audio' Message announced in the role="alert" overlay.

See Options for the full surface and Audio modes for how external-mode events drive the slider.