SFX attaches audio to authored moments and ordinary browser events. A cue plays once; a loop continues until stopped. The viewer controls listening through shared mute and volume settings.
bun run pmx plugins add sfx
This activates @pathmx/sfx. Install Player
for Block and Beat choreography. Custom event triggers work without Player.
Effects are invisible by default. Mute and volume are shared viewer preferences,
available in site Settings when the Settings UI is installed. For a custom layout,
optionally place these controls once alongside other shared preferences:
<x-sfx-toggle />
<x-sfx-volume />
Keep listening controls out of individual effects. Visible playback buttons are authored explicitly, as in the literate component example below.
Controls share one preference and reflect each other's changes. With PathMX Settings, preferences persist for this site on this device. Without Settings, they last for the document session. Defaults are sound enabled at 70% volume; the browser still requires a user interaction to initialize audio.
Try the authored sound example.
# The reveal
<x-sfx-cue src="./sounds/reveal.mp3" on="player:block-enter" />
The unexpected result.
Without for, on and off listen on the containing Block. SFX declarations
are hidden and do not become Beats. A loop can follow the same moment:
<x-sfx-loop
src="./sounds/ambience.mp3"
on="player:block-enter"
off="player:block-leave"
/>
For an individual Beat, give its element an ID and name it with for:
<p id="alternative" data-pmx-beat="alternative">
Now consider the alternative.
</p>
<x-sfx-cue
src="./sounds/tick.mp3"
for="alternative"
on="player:beat-enter"
/>
for is an element ID without #, not a CSS selector. Targets live in the same
browser document; embedded pages own their own bindings. Ordinary DOM bubbling
applies to custom events. Player bindings match the target element itself so a
Block listener does not accidentally consume its descendant Beats' events.
Missing runtime targets produce a console diagnostic and stay silent.
| Component | Attributes | Behavior |
|---|---|---|
x-sfx-cue | Required src; optional on, off, for, volume, id, class | Plays one sound per trigger; another trigger restarts it. |
x-sfx-loop | Same as cue | Repeats until stopped. |
x-sfx-toggle | Optional id, class | Mutes or unmutes all SFX in the document. |
x-sfx-volume | Optional id, class | Adjusts the shared volume with a native range input. |
src accepts a local audio asset or HTTPS URL, using a format the browser can
decode. Relative paths resolve beside the Source that authors the component.
Remote audio must permit cross-origin fetching. A sound's volume is a number
from 0 to 1 multiplied by the viewer's volume. It cannot override mute.
A cue needs no automatic binding when its component controls playback directly.
Use this HTML in a component's html fence:
<div>
<button type="button" data-pmx-play-affordance data-pmx-play-icon="play">Try sound</button>
<x-sfx-cue class="feedback-sound" src="./sounds/success.mp3" />
</div>
The button also appears in Player’s Beat actions,
which activate the same authored control. Its js fence can use instance-scoped helpers:
on("button", "click", () => {
$(".feedback-sound").dispatchEvent(new Event("sfx:play"))
})
Send sfx:stop to the same element to stop it. Commands are ordinary events
addressed directly to that cue or loop. Instance-scoped selection keeps repeated
components independent without a global sound registry.
For outcomes with multiple consumers, emit a domain event from an element:
answerElement.dispatchEvent(
new CustomEvent("exercise:correct", {
bubbles: true,
detail: { attempt: 2 },
}),
)
If that element has id="answer", an authored sound can bind with
for="answer" on="exercise:correct". IDs must be unique within the document;
prefer direct instance-scoped commands inside repeated component templates.
SFX matches the event name and does not interpret domain data.
Player-triggered sounds play on starting Play or moving forward. Backward movement, scroll adoption, history restoration, and live updates stay quiet. Opening a page with a Play URL restores its position silently. Custom events and direct SFX commands play when explicitly triggered, including outside Play.
Leaving a Player-bound moment cancels its pending audio and stops playback.
Loops also stop on their off event, removal, or page exit. A live update that
preserves the declaration and its target preserves playback; changing its asset
or binding stops it without automatically replaying it.
Mute immediately silences playback. Short effects triggered while muted are ignored and never replayed. An active loop resumes on unmute only while its moment remains active. Volume zero follows the same silence/resume behavior; changing volume does not clear the mute preference.
Audio initializes on a pointer or keyboard interaction. Effects triggered before initialization are not queued. Audio loading failures remain local to the sound; Player navigation and the rest of the content remain usable. SFX uses browser playback and ordinary assets in both live sites and static publication.
See Player lifecycle events for the event payload and ordering. These DOM events are separate from committed Events and analytics observations.