Documentation

Everything cuelume exports, and when to reach for it.

Seven exports and seven attributes. Most projects only ever needbind(). The rest is here for sounds at an exact moment, a second material, or a listener who wants it quieter.

Install

Install Cuelume, then import it wherever your interface runs.

$

Click the command to switch package manager. ESM-only, zero runtime dependencies, and safe to import during SSR.

Quick start

Mark up the elements that should make a sound, then wire them all at once. Nothing else is needed.

index.html
<button data-cuelume-tap>Save</button>
<input data-cuelume-type placeholder="Search">
<button data-cuelume-open>Open menu</button>
<a href="/settings" data-cuelume-navigate>Settings</a>
app.ts
import { bind } from "cuelume";

bind();

Attributes

Each attribute covers one behavior. Leave the value empty to use its default sound, or set it to any name from thecatalog.

AttributeFires onDefault
data-cuelume-tapFires onclickDefaulttap
data-cuelume-typeFires onkeydown (edits)Defaulttype
data-cuelume-selectFires onchange, else clickDefaultselect
data-cuelume-toggleFires onclickDefaulttoggle
data-cuelume-openFires onclickDefaultopen
data-cuelume-closeFires onclickDefaultclose
data-cuelume-navigateFires onclickDefaultnavigate

Add data-cuelume-emphasis="subtle" or"strong" to an element or any ancestor to set how much the action matters.data-cuelume-theme="bubble" sets the material the same way.

bind()

bind(root?: ParentNode): void

Delegates every data-cuelume-* interaction under root, which defaults to the whole document. Call it once at startup.

It is idempotent, so calling it again is harmless, and it handles elements added later, so you do not need to re-bind after a render.

app.ts
import { bind } from "cuelume";

bind();                               // the whole document
bind(document.querySelector("#app")); // or one subtree

play()

play(name?: SoundName, options?: PlayOptions): void

Plays a sound immediately, for moments that are not a DOM interaction: a request that resolved, a copy that landed, a form that failed. Defaults to tap.

Options apply to this play only: volume,emphasis, the context a binding would detect (direction, key,input), a theme for this play, and a duration forcount. Unknown values are ignored.

app.ts
import { play } from "cuelume";

play("loading");
try {
  await exportReport();
  play("ready");                         // the file is there
} catch {
  play("error");
}

play("navigate", { direction: "back" }); // the whoosh falls
play("success", { emphasis: "strong" }); // a publish, not an autosave
play("success", { volume: 0.4 });        // quieter, just this once
play("count", { duration: 900 });        // a number rolling to its new value

Browsers block audio on a fresh visit until the listener interacts. Play navigate on client-side navigations after that first interaction.

setTheme()

setTheme(theme: "default" | "mech" | "bubble" | "press"): void

Switches the material of future playback. All four themes carry the same fourteen cues at the same levels, so a switch changes how they sound, not what they mean. For one playful moment, passtheme to play() or setdata-cuelume-theme instead. Unknown names are ignored and the choice is not persisted.

app.ts
import { play, setTheme } from "cuelume";

setTheme("mech");    // dry, machined parts
setTheme("bubble");  // knocks, drips, corks, and gulps
setTheme("press");   // a crisp click over a warm, swelling note
setTheme("default"); // glass, wood, air, soft mallets

play("select", { theme: "bubble" }); // one playful moment

setVolume()

setVolume(volume: number): void

Sets the global level for future playback, clamped between 0 and 1. Non-finite values are ignored.

Your app owns the setting: cuelume applies it but never persists it, so wire it to whatever preference store you already have.

app.ts
import { setVolume } from "cuelume";

setVolume(0.7);
setVolume(Number(localStorage.getItem("ui-volume") ?? 1));

setEnabled()

setEnabled(enabled: boolean): void

Turns future playback on or off. Sounds already ringing are left to finish, and like the volume, the preference is not persisted.

app.ts
import { setEnabled } from "cuelume";

setEnabled(false); // further play attempts become no-ops
setEnabled(true);

sounds

sounds: readonly SoundName[]

Every sound name, in catalog order. Useful for building a picker without hardcoding the list.

app.ts
import { sounds, play } from "cuelume";

sounds.forEach((name) => {
  // one button per cue
});

SoundName

type SoundName = "tap" | "type" | "select" | "toggle" | "open" | "close" | "success" | "error" | "navigate" | "warning" | "loading" | "ready" | "attention" | "count"

The union of the fourteen cue names. Import it as a type to keep your own sound maps honest.

app.ts
import { play, type SoundName } from "cuelume";

const forStatus: Record<"ok" | "warn" | "bad", SoundName> = {
  ok: "success",
  warn: "warning",
  bad: "error",
};

play(forStatus[status]);

Sound catalog

Fourteen cues, each with its own shape. Press one to hear it.

Which cue?

Pick by the job, not by the sound. Every row uses a cue that exists; emphasis and options do the rest. Never play one per streamed token, per tool call, or on hover: play one when the run ends. An animated number gets one count, never one per digit.

MomentCueHow
Primary button: save, send, submitCuetapHow—
Secondary or ghost buttonCuetapHowsubtle
Destructive confirm: delete, removeCuecloseHowstrong
Copy to clipboardCuesuccessHowsubtle
Like, star, bookmarkCuetoggleHow—
Undo / redoCuenavigateHowsubtle, direction "back" / "forward"
Keystroke, delete, space, returnCuetypeHowbindings set key
Autocomplete acceptedCueselectHow—
Field fails validationCueerrorHowsubtle
Menu, dropdown, or list optionCueselectHow—
Tab, segmented control, radioCueselectHowbindings set direction
Checkbox, switchCuetoggleHowbindings set direction
Slider or stepper stepCueselectHowsubtle, direction
Menu or popover opens / closesCueopen / closeHowsubtle
Dialog or drawer opens / closesCueopen / closeHow—
Accordion expands / collapsesCueopen / closeHowsubtle
Command paletteCueopenHowsubtle, or nothing
ToastCuethe cue for what it reportsHowsubtle
Route changeCuenavigateHow—
Back buttonCuenavigateHowdirection "back"
Carousel, gallery, paginationCuenavigateHowsubtle, direction
Drag picked up / dropped / cancelledCueselect / tap / closeHow— / strong / subtle
Reorder a list itemCueselectHowdirection
Saved, syncedCuesuccessHowsubtle
Payment, publish, deploy confirmedCuesuccessHowstrong
Partial failure, deprecation, connection retryingCuewarningHow—
Form rejected, permission deniedCueerrorHow—
Upload, export, or build startedCueloadingHow—
Upload finishedCuesuccessHow—
Export ready to downloadCuereadyHow—
Long job finished while the user was awayCuereadyHowstrong
New message in an open conversationCuereadyHowsubtle
Reminder or timer dueCueattentionHow—
A number animates to a new valueCuecountHowduration matching the animation
A number counts downCuecountHowdirection "back"
A live number that updates constantly: price feed, viewersCuenothingHow—
AI: prompt sent / generation startedCuetap / loadingHow— / subtle
AI: generation stoppedCuecloseHow—
AI: reply finished streamingCuereadyHowsubtle
AI: tool call needs approvalCueattentionHow—
AI: agent task doneCuesuccessHow—
AI: model, tool, or network failureCueerrorHow—
AI: suggestion accepted / rejectedCueselect / closeHow— / subtle
AI: switch model or modeCueselectHow—
One playful moment in a professional app, e.g. the AI model pickerCuethe usual cueHowtheme "bubble"

Behavior

The defaults are opinionated so you do not have to be.

One cue per action
An event plays at most one cue, even with nested marked elements or several bound roots. Click bindings follow native activation, so Enter and Space play too.
Typing that behaves
Only keys that edit text play, never in a password field, at most once every 40ms. Holding Backspace keeps playing until there is nothing left to delete.
Audible without clipping
One shared boosted output stage keeps cues clear, with native compression protecting overlapping sounds.
Struck, not played back
Tones are modelled on real bars: each overtone dies faster the higher it is, and a brief mallet contact starts the note. Every play of a frequent cue is one strike, a little harder or softer than the last, so no two sound identical. Outcome cues play the same every time.
One faint room
Every cue rings very faintly into one shared, short stereo room, so it sounds placed around the listener rather than inside their head.
One lazy AudioContext
Shared across every sound and created on first use, not on import.
Autoplay-friendly
Suspended audio is resumed where possible, without surfacing an error when a browser blocks it.
SSR-safe
Importing on the server is a no-op.

Anything missing?

Open an issue on GitHub, or point your coding agent at agents.md and it can wire cuelume into a project on its own.