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.
Two focusable surfaces
Section titled “Two focusable surfaces”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 -1 → 0) |
Space, digits 0–9, 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.
Keyboard controls
Section titled “Keyboard controls”The seek slider
Section titled “The seek slider”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.
The player root
Section titled “The player root”Click a non-interactive area of the player to focus its root, then:
| Key | Action | Mode |
|---|---|---|
| Space | Toggle play / pause | Both |
| 0–9 | 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).
The accessibleSeek slider
Section titled “The accessibleSeek slider”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).
new WaveformPlayer('#player', { url: '/audio/track.mp3', accessibleSeek: false,});Naming the slider
Section titled “Naming the slider”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.
new WaveformPlayer('#player', { url: '/audio/interview.mp3', title: 'Episode 42', seekLabel: 'Seek within Episode 42',});<div data-waveform-player data-src="/audio/interview.mp3" data-title="Episode 42" data-seek-label="Seek within Episode 42"></div>The spoken value text
Section titled “The spoken value text”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.
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.
Other ARIA labelling
Section titled “Other ARIA labelling”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".
Localizing the interface
Section titled “Localizing the interface”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 |
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',});<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>Localization cookbook
Section titled “Localization cookbook”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',}{ playPauseLabel: 'Reproducir/Pausar', speedLabel: 'Velocidad de reproducción', artworkAlt: 'Portada del álbum', errorText: 'No se pudo cargar el audio', seekValueText: '%1$s de %2$s', unknownTrackText: 'Pista desconocida',}{ playPauseLabel: 'Wiedergabe/Pause', speedLabel: 'Wiedergabegeschwindigkeit', artworkAlt: 'Albumcover', errorText: 'Audio konnte nicht geladen werden', seekValueText: '%1$s von %2$s', unknownTrackText: 'Unbekannter Titel',}{ playPauseLabel: 'Riproduci/Pausa', speedLabel: 'Velocità di riproduzione', artworkAlt: 'Copertina dell’album', errorText: 'Impossibile caricare l’audio', seekValueText: '%1$s di %2$s', unknownTrackText: 'Traccia sconosciuta',}{ playPauseLabel: 'Reproduzir/Pausar', speedLabel: 'Velocidade de reprodução', artworkAlt: 'Capa do álbum', errorText: 'Não foi possível carregar o áudio', seekValueText: '%1$s de %2$s', unknownTrackText: 'Faixa desconhecida',}{ playPauseLabel: 'Воспроизведение/Пауза', speedLabel: 'Скорость воспроизведения', artworkAlt: 'Обложка альбома', errorText: 'Не удалось загрузить аудио', seekValueText: '%1$s из %2$s', unknownTrackText: 'Неизвестный трек',}// 简体中文 (Simplified){ playPauseLabel: '播放/暂停', speedLabel: '播放速度', artworkAlt: '专辑封面', errorText: '无法加载音频', seekValueText: '%1$s / %2$s', unknownTrackText: '未知曲目',}// 日本語 — reorders to "Y 中 X" (X of Y){ playPauseLabel: '再生/一時停止', speedLabel: '再生速度', artworkAlt: 'アルバムアートワーク', errorText: '音声を読み込めませんでした', seekValueText: '%2$s 中 %1$s', unknownTrackText: '不明なトラック',}// 한국어 — reorders to "Y 중 X" (X of Y){ playPauseLabel: '재생/일시정지', speedLabel: '재생 속도', artworkAlt: '앨범 아트워크', errorText: '오디오를 불러올 수 없습니다', seekValueText: '%2$s 중 %1$s', unknownTrackText: '알 수 없는 트랙',}// العربية — right-to-left (see note below){ playPauseLabel: 'تشغيل/إيقاف مؤقت', speedLabel: 'سرعة التشغيل', artworkAlt: 'صورة الألبوم', errorText: 'تعذّر تحميل الصوت', seekValueText: '%1$s من %2$s', unknownTrackText: 'مقطع غير معروف',}{ playPauseLabel: 'Afspelen/Pauzeren', speedLabel: 'Afspeelsnelheid', artworkAlt: 'Albumhoes', errorText: 'Kan audio niet laden', seekValueText: '%1$s van %2$s', unknownTrackText: 'Onbekend nummer',}{ playPauseLabel: 'Odtwórz/Wstrzymaj', speedLabel: 'Prędkość odtwarzania', artworkAlt: 'Okładka albumu', errorText: 'Nie można załadować dźwięku', seekValueText: '%1$s z %2$s', unknownTrackText: 'Nieznany utwór',}{ playPauseLabel: 'Oynat/Duraklat', speedLabel: 'Oynatma hızı', artworkAlt: 'Albüm kapağı', errorText: 'Ses yüklenemedi', seekValueText: '%1$s / %2$s', unknownTrackText: 'Bilinmeyen parça',}{ playPauseLabel: 'Spela/Pausa', speedLabel: 'Uppspelningshastighet', artworkAlt: 'Albumomslag', errorText: 'Det gick inte att läsa in ljudet', seekValueText: '%1$s av %2$s', unknownTrackText: 'Okänt spår',}// हिन्दी — reorders to "Y में से X" (X of Y){ playPauseLabel: 'चलाएँ/रोकें', speedLabel: 'प्लेबैक गति', artworkAlt: 'एल्बम कलाकृति', errorText: 'ऑडियो लोड नहीं हो सका', seekValueText: '%2$s में से %1$s', unknownTrackText: 'अज्ञात ट्रैक',}{ playPauseLabel: 'Putar/Jeda', speedLabel: 'Kecepatan pemutaran', artworkAlt: 'Sampul album', errorText: 'Tidak dapat memuat audio', seekValueText: '%1$s dari %2$s', unknownTrackText: 'Trek tidak dikenal',}{ playPauseLabel: 'Phát/Tạm dừng', speedLabel: 'Tốc độ phát', artworkAlt: 'Ảnh bìa album', errorText: 'Không thể tải âm thanh', seekValueText: '%1$s / %2$s', unknownTrackText: 'Bản nhạc không xác định',}{ playPauseLabel: 'เล่น/หยุดชั่วคราว', speedLabel: 'ความเร็วในการเล่น', artworkAlt: 'ปกอัลบั้ม', errorText: 'ไม่สามารถโหลดเสียงได้', seekValueText: '%1$s จาก %2$s', unknownTrackText: 'แทร็กที่ไม่รู้จัก',}{ playPauseLabel: 'Відтворення/Пауза', speedLabel: 'Швидкість відтворення', artworkAlt: 'Обкладинка альбому', errorText: 'Не вдалося завантажити аудіо', seekValueText: '%1$s з %2$s', unknownTrackText: 'Невідомий трек',}{ playPauseLabel: 'Přehrát/Pozastavit', speedLabel: 'Rychlost přehrávání', artworkAlt: 'Obal alba', errorText: 'Nelze načíst zvuk', seekValueText: '%1$s z %2$s', unknownTrackText: 'Neznámá stopa',}Visible focus
Section titled “Visible focus”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:
.waveform-player { --wfp-accent: #6366f1;}prefers-reduced-motion
Section titled “prefers-reduced-motion”The stylesheet respects the OS “reduce motion” setting. Under @media (prefers-reduced-motion: reduce), every transition and animation inside .waveform-player is neutralised:
@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.
Media Session
Section titled “Media Session”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 |
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 and assistive tech
Section titled “External mode and assistive tech”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 viasetProgress(). - Arrow / Page / Home / End seeking and Space still work — they dispatch cancelable
waveformplayer:request-seek/request-play/request-pauseevents for the controller to honour. aria-valuenow/aria-valuetextupdate fromsetProgress(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.
Options reference
Section titled “Options reference”| 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. |
| Attribute | Maps to |
|---|---|
data-accessible-seek |
accessibleSeek ("true" / "false") |
data-seek-label |
seekLabel |
data-seek-value-text |
seekValueText |
data-play-pause-label |
playPauseLabel |
data-speed-label |
speedLabel |
data-enable-media-session |
enableMediaSession |
data-title |
title |
data-unknown-track-text |
unknownTrackText |
data-artist |
artist |
data-album |
album |
data-artwork |
artwork |
data-artwork-alt |
artworkAlt |
data-error-text |
errorText |
See Options for the full surface and Audio modes for how external-mode events drive the slider.