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>Other ARIA labelling
Section titled “Other ARIA labelling”Beyond the slider, the rendered DOM is labelled throughout:
| Element | CSS class | Accessibility |
|---|---|---|
| Play / pause button | .waveform-btn |
aria-label="Play/Pause" |
| Speed menu trigger | .speed-btn |
aria-label="Playback speed" |
| Cue markers | .waveform-marker |
aria-label set to each marker’s label |
| Error overlay | .waveform-error |
role="alert" — announced when load fails |
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".
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 "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". |
enableMediaSession |
boolean |
true |
Register system Media Session controls (self mode only). |
title |
string | null |
null |
Media Session title + slider name fallback. |
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). |
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-enable-media-session |
enableMediaSession |
data-title |
title |
data-artist |
artist |
data-album |
album |
data-artwork |
artwork |
data-error-text |
errorText |
See Options for the full surface and Audio modes for how external-mode events drive the slider.