Audio Player
Plays one voice note back inline: a round play button, a scrub bar, a duration, and an optional delete.
ws_audio_player() emits a real <audio preload="none"> plus data-player-action hooks. Play, pause, scrub, the progress fill and the delete confirmation all live in FacilitatorVoiceNotes (facilitator/js/modules/voice-notes.js), which is not part of components.js. The examples on this page are therefore static: the buttons will not play anything here.
When to Use
Variants
There is no variant option. What changes between these is which of the four content options are present, and each one adds or removes a part.
Source only
The minimum call. With no duration the time reads 0:00 until playback starts and the JS begins writing remaining time into it.
<?= ws_audio_player('/uploads/voice_notes/file.webm') ?>With a duration
Pass seconds, get m:ss. Always pass it: the stored value is known server-side, and without it the row shows a false 0:00 for a note that is two minutes long.
<?= ws_audio_player($note['file_path'], ['duration' => $note['duration']]) ?>
With a label
The label sits under the scrub bar in --text-micro and truncates with an ellipsis rather than wrapping, so the row keeps its height whatever the note is called.
<?= ws_audio_player($note['file_path'], [
'duration' => $note['duration'],
'label' => $note['label'],
]) ?>Deletable
deletable adds the trailing close button; note_id is what makes it work. The JS reads data-note-id off the root and does nothing at all when it is missing, so the two options only make sense together.
<?= ws_audio_player($note['file_path'], [
'duration' => $note['duration'],
'label' => $note['label'],
'note_id' => $note['id'],
'deletable' => true,
]) ?>Narrow column
The scrub bar takes the slack, so the control survives a sidebar width. The label truncates first, then the bar shortens.
States
One class carries the only state: .ws-audio-player--playing, which swaps the glyph. Everything else is the browser's. The second example below is the playing state, faked through the class option plus a demo rule on the fill, since nothing here is actually playing.
<?= ws_audio_player($src, ['class' => 'ws-audio-player--playing']) ?> <!-- In production the JS adds and removes this class; do not ship it. -->
| State | Behavior |
|---|---|
| Paused | Default. play_arrow shows, pause is hidden by the :not(--playing) rule as well as by its own hidden attribute. |
| Playing | .ws-audio-player--playing on the root swaps the glyphs. The JS pauses every other player on the page first, so two notes never talk over each other. |
| Progress | .ws-audio-player__fill is driven by inline width from timeupdate, with a 0.1s linear transition so it moves smoothly rather than ticking. |
| Time readout | Shows the total duration at rest and counts down the remaining time while playing. It never shows elapsed time, which surprises people reading the code. |
| Ended | The playing class is dropped and the fill resets to 0%. The row returns to its resting look. |
| Seek | Clicking anywhere on .ws-audio-player__progress jumps to that ratio of the duration. Pointer only, see Accessibility. |
| Hover | The play button darkens; the delete button turns --color-error. The row itself has no hover treatment. |
| Missing file | No handling. A 404 on the src leaves a control that looks live and does nothing when pressed. |
| Disabled | Not supported. There is no option and no styling for it. |
Real-World Usage
The Facilitator's voice note list. Notes saved earlier in the session come back from the API and render server-side; notes recorded during the session are appended client-side, which is why the same markup exists twice.
<?php foreach ($notes as $note): ?>
<?= ws_audio_player($note['file_path'], [
'duration' => $note['duration'],
'label' => $note['label'],
'note_id' => $note['id'],
'deletable' => true,
]) ?>
<?php endforeach; ?>
<script>
// Bind after render, and again after appending any new player.
FacilitatorVoiceNotes.initPlayers(document.getElementById('voice-list'));
</script>voice-notes.js constructs a .ws-audio-player string for notes recorded in-session rather than calling back to PHP. Any change to the template has to be mirrored there or the two copies drift, which is exactly the duplication a component is meant to prevent. Check both before editing either.
Options
| Option | Type | Default | Purpose |
|---|---|---|---|
$src | string | '' | Positional and required in practice. Written to both data-src on the root and the <audio src>. |
duration | int | 0 | Seconds. Cast with (int) and formatted as m:ss; 0 or absent renders 0:00. The design system JSON lists this as a string, which is wrong: pass an integer. |
label | string | '' | Caption under the scrub bar. Omitted entirely when empty, so the row shrinks rather than leaving a gap. |
note_id | int|string | '' | Renders data-note-id, which the delete handler needs. Omitted when empty, so the attribute is absent rather than blank. |
deletable | bool | false | Adds the trailing delete button. Pair it with note_id or the button is inert. |
id | string | 'ws-player-' . uniqid() | Root element ID. Auto-generated; set it only if you need a stable handle. |
class | string | '' | Extra classes on the root. |
attrs | array | [] | Extra HTML attributes on the root. Every entry renders as key="value"; there is no boolean-attribute form. |
Accessibility
| Concern | Behavior |
|---|---|
| Play button | A real <button> with aria-label="Play audio". Gap: the label is static, so it still says "Play audio" while the note is playing and the button actually pauses. |
| Delete button | Uses title for its name rather than aria-label. Announced by most screen readers, but title is the weaker hook and it is invisible to touch users. |
| Scrub bar | Gap. .ws-audio-player__progress is a <div> with a click handler: no tabindex, no role="slider", no arrow-key handling. Seeking is pointer-only. Play and pause are reachable, so the note is still listenable end to end. |
| Time readout | Plain text with no live region, which is correct here: a per-second countdown announced aloud would be unusable. |
| Colour | The progress fill is --color-primary on --gray-100, decorative rather than informational, and the same information is in the time readout. |
| Loading | preload="none", so a list of twenty notes costs no bandwidth until one is played. Nothing signals the fetch, so a slow first press looks like nothing happened. |
Tokens
| Token | Used for |
|---|---|
--color-primary | Play button fill and the progress fill |
--text-inverse | Glyph on the play button |
--gray-100 / --gray-200 | Progress track, row border |
--bg-surface | Row ground |
--text-secondary / --text-tertiary | Time readout, label, delete glyph |
--color-error | Delete glyph on hover |
--radius-md / --radius-sm | Row shape, delete hit area |
--space-2 / --space-3 | Row padding and the gaps between parts |
--font-body | Face for label and time |
--text-micro / --text-caption / --text-body / --text-h4 | Label, time, delete glyph, play glyph |
.ws-audio-player__btn:hover resolves var(--color-primary-dark, #651FFF), and --color-primary-dark is not defined in platform-tokens.css. Outside Coach and the detail sidebar, which each define it locally, the play button hovers to a deep purple left over from before Red Unification. Worth fixing in components.css, which this page deliberately does not do.
CSS Classes
| Class | Purpose |
|---|---|
.ws-audio-player | Root row, carrying data-src and optionally data-note-id |
.ws-audio-player--playing | Set by JS during playback; swaps play for pause |
.ws-audio-player__btn | The round play or pause button |
.ws-audio-player__icon--play / __icon--pause | The two glyphs, one hidden at a time |
.ws-audio-player__track | Column holding the scrub bar and the label |
.ws-audio-player__progress / __fill | Scrub bar and its inline-width fill |
.ws-audio-player__label | Caption, truncated with an ellipsis |
.ws-audio-player__time | Duration at rest, remaining time while playing |
.ws-audio-player__delete | Trailing delete, rendered only when deletable |
The JS contract is data-player-action, with values toggle and delete, plus the <audio> element inside the root. Renaming any of those breaks playback.
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_audio_player() helper function |
includes/components/audio-player.php | Template, including the m:ss formatting |
includes/components/components.css | Styles (.ws-audio-player rules) |
facilitator/js/modules/voice-notes.js | FacilitatorVoiceNotes.initPlayers() and the hand-built markup copy |
api/upload-voice-note.php | Serves the note list and handles delete |
design-system/components/php/AudioPlayer.php | Portable namespaced mirror of the helper |