Mark up your HTML with data-sound-* attributes and call bind() once, or call play() directly whenever you need exact control.
$ npm install chordal
ESM-only, zero runtime dependencies, safe to import during SSR.
<button data-sound-hover data-sound-click>Save</button>
import { bind } from 'chordal'; bind();
bind() scans your markup once and wires up every data-sound-* element it finds.
| Attribute | Fires on | Instance |
|---|---|---|
| data-sound-hover | pointerenter | hover |
| data-sound-click | pointerdown | click |
| data-sound-success | click | success |
| data-sound-error | invalid | error |
| data-sound-toggle | click | toggle |
| data-sound-sent | click | sent |
| data-sound-notification | click | notification |
data-sound-click="chime") to override just that element.data-sound-* attribute at all — listening and delete take a call each to start and stop or confirm (see play()), and slider is driven by playContinuous(), not a discrete DOM event. Call these directly.data-sound-toggle is the only attribute requiring custom setup. It reads aria-pressed (or checked) at click time—wire your toggle logic first so it reads the updated state:
<button id="dark-mode-toggle" aria-pressed="false" data-sound-toggle>Dark mode</button>
const toggleBtn = document.querySelector('#dark-mode-toggle'); toggleBtn.addEventListener('click', () => { const isOn = toggleBtn.getAttribute('aria-pressed') === 'true'; toggleBtn.setAttribute('aria-pressed', String(!isOn)); }); bind(); // after your own toggle logic — reads aria-pressed at click time
Already have the state in your own code instead of reading it off the DOM? Call play('toggle', { state }) directly — see play().
Scans root (defaults to document) for attributes and wires listeners. Safe to call again: already-bound elements are skipped.
The direct, imperative trigger, for moments that are not a DOM interaction: an async action that just resolved, or a state your own code already knows about.
toggle and listening are stateful — pass which state just became active:
play('toggle', { state: 'on' }); // already know the state? skip data-sound-toggle entirely play('listening', { state: 'on' }); // mic session started play('listening', { state: 'off' }); // mic session stopped
For the declarative data-sound-toggle attribute instead — reading state off the DOM at click time — see Toggle.
delete has no data-sound-delete attribute — call it yourself, after your own confirmation step, not on a raw click:
await confirmDelete(item); play('delete'); item.remove();
For a range slider's input event, pass the current 0–1 ratio. Debounced and pitch-quantized internally, so fast dragging reads as musical steps, not a siren sweep:
<input type="range" id="volume" min="0" max="100" value="50" />
import { playContinuous } from 'chordal'; const slider = document.querySelector('#volume'); slider.addEventListener('input', () => { playContinuous('slider', Number(slider.value) / 100); });
The family used when a call doesn't override its own.
Global on/off switch for the master output.
Exported readonly arrays for building your own picker UI.
SoundFamily and SoundInstance are also exported as types — the same values, at compile time — for keeping your own lookup maps honest:
import { setFamily, type SoundFamily } from 'chordal'; const familyForTheme: Record<'light' | 'dark', SoundFamily> = { light: 'glass-crystal', dark: 'metallic-tact', }; setFamily(familyForTheme[theme]);
Embed <chordal-playground> for an interactive family/instance browser. It's a dev-time tool, not for production pages.
A condensed reference built for AI coding agents: install steps, the family/instance model, and the behavioral details a generated integration would otherwise get wrong.
View AGENTS.md ↗