All 17 semantic sound roles across 5 categories with TypeScript types and role registry.
sensory-ui defines 17 semantic sound roles organized into 5 categories. Each role maps to a specific UI interaction type. No random audio, every sound has meaning.
4 roles · taps, toggles, confirms.
4 roles · open, close, expand, collapse.
3 roles · forward, backward, tab.
4 roles · info to error.
2 roles · celebrations, opt-in.
| Category | Count | Default Enabled | Duration Range | Character |
|---|---|---|---|---|
interaction | 4 | Yes | 8–50 ms | Short transients, soft attack |
overlay | 4 | Yes | 120–270 ms | Tonal pairs (open ↔ close) |
navigation | 3 | Yes | 40–250 ms | Low-volume sweeps, directional |
notification | 4 | Yes | 200–450 ms | Two-tier intensity |
hero | 2 | No | 350–1900 ms | Celebratory, longer form |
Durations are base tune lengths: the active pack's instrument stretches them via its decay multiplier, and hero sounds are hand-crafted per pack so they vary the most.
interactionFired when the user performs a direct action: pressing a button, toggling a state, confirming a form, or providing subtle feedback during continuous interactions.
| Role | Full Key | Duration | Description | Typical Trigger |
|---|---|---|---|---|
| Tap | interaction.tap | ~8 ms | Default click/tap sound. Noise click, bandpass 3800 Hz. | Primary button click |
| Subtle | interaction.subtle | ~8 ms | Keyboard click. Crisp bandpass ~3600 Hz, tighter Q. | Slider drag, command input keypress |
| Toggle | interaction.toggle | ~35 ms | FM pitch-glide pop (700 → 480 Hz). State change. | Checkbox, switch, radio, toggle |
| Confirm | interaction.confirm | ~12 ms | Crisp noise click, bandpass 5500 Hz. Brighter than tap. | Form submit, save confirm |
There is no interaction.disabled role because disabled buttons cannot be clicked,
so no sound is needed.
overlayFired when UI surfaces open, close, expand, or collapse. These are structural changes to the visual layout.
| Role | Full Key | Duration | Description | Typical Trigger |
|---|---|---|---|---|
| Open | overlay.open | ~180 ms | A gentle expansion sound. | Dialog open, sheet open, dropdown open |
| Close | overlay.close | ~180 ms | Tonal inverse of open. Collapse. | Dialog close, sheet close, dropdown close |
| Expand | overlay.expand | ~120 ms | Lighter than open. Content reveals. | Accordion expand, collapsible open |
| Collapse | overlay.collapse | ~120 ms | Paired with expand. Content hides. | Accordion collapse, collapsible close |
overlay.open / overlay.close are a tonal inversion pair: if open
rises in pitch, close falls. The same applies to expand/collapse.
navigationFired when the user moves through space: between pages, tabs, or steps.
| Role | Full Key | Duration | Description | Typical Trigger |
|---|---|---|---|---|
| Forward | navigation.forward | ~140 ms | A rightward / upward sweep. | Next step, next page, carousel next |
| Backward | navigation.backward | ~140 ms | A leftward / downward sweep. Tonal mirror of forward. | Back button, previous step, carousel prev |
| Tab | navigation.tab | ~40 ms | Short tonal pop with upward pitch bend. | Tab switch, segment switch, pagination |
navigation.forward and navigation.backward should be tonal inversions
of each other (same harmonic content, opposite pitch arc).
notificationFired when the system delivers feedback to the user: toasts, alerts, banners, inline validation messages.
| Role | Full Key | Duration | Description | Typical Trigger |
|---|---|---|---|---|
| Info | notification.info | ~220 ms | Neutral information. Soft single chime. | Info toast, passive alert |
| Success | notification.success | ~450 ms | Three ascending notes, C5–E5–G5 (positive feeling). | Success toast, form saved |
| Warning | notification.warning | ~350 ms | Two-note semitone ascent, A4 → A♯4 (attention-getting). | Quota alert, confirmation needed |
| Error | notification.error | ~200 ms | Filtered descending sweep, 320 → 140 Hz (negative connotation). | Error toast, connection failed |
heroFired for significant completion moments: task completion, onboarding milestones, achievement unlocks. These are special-occasion sounds and are disabled by default.
| Role | Full Key | Duration | Description | Typical Trigger |
|---|---|---|---|---|
| Complete | hero.complete | ~650–1900 ms | Task done. Multi-note fanfare, hand-crafted per pack. | Checklist complete, upload done |
| Milestone | hero.milestone | ~350–1100 ms | Lighter celebration. Shorter than complete. | Onboarding step done, streak saved |
Hero sounds are disabled by default. You must explicitly enable the category
(config={{ categories: { hero: true } }}) and then assign the sound prop, a double opt-in that
prevents accidental hero sounds in production.
// components/ui/sensory-ui/config/sound-roles.ts
export type SoundCategory =
| "interaction"
| "overlay"
| "navigation"
| "notification"
| "hero";
export type InteractionRole =
| "interaction.tap"
| "interaction.subtle"
| "interaction.toggle"
| "interaction.confirm";
export type OverlayRole =
| "overlay.open"
| "overlay.close"
| "overlay.expand"
| "overlay.collapse";
export type NavigationRole =
| "navigation.forward"
| "navigation.backward"
| "navigation.tab";
export type NotificationRole =
| "notification.info"
| "notification.success"
| "notification.warning"
| "notification.error";
export type HeroRole = "hero.complete" | "hero.milestone";
export type SoundRole =
| InteractionRole
| OverlayRole
| NavigationRole
| NotificationRole
| HeroRole;export function parseRole(role: SoundRole): {
category: SoundCategory;
name: string;
} {
const [category, name] = role.split(".") as [SoundCategory, string];
return { category, name };
}export const ALL_SOUND_ROLES: SoundRole[] = [
"interaction.tap",
"interaction.subtle",
"interaction.toggle",
"interaction.confirm",
"overlay.open",
"overlay.close",
"overlay.expand",
"overlay.collapse",
"navigation.forward",
"navigation.backward",
"navigation.tab",
"notification.info",
"notification.success",
"notification.warning",
"notification.error",
"hero.complete",
"hero.milestone",
];The role registry maps each SoundRole string to a SoundSynthesizer function within a sound pack:
// components/ui/sensory-ui/config/registry.ts
import type { SoundRole } from "./sound-roles";
import type { SoundSource } from "./engine";
// SoundPackName is defined in sounds/packs.ts as
// `keyof typeof soundPacks` and re-exported here:
import type { SoundPackName } from "../sounds/packs";
export type SoundPack = Record<SoundRole, SoundSource>;
export const packRegistry: Record<SoundPackName, SoundPack> = {
soft: softPack,
aero: aeroPack,
arcade: arcadePack,
organic: organicPack,
glass: glassPack,
industrial: industrialPack,
minimal: minimalPack,
retro: retroPack,
crisp: crispPack,
};
export const DEFAULT_PACK: SoundPackName = "aero";When providing custom audio source overrides via config.overrides, files should meet these requirements:
| Requirement | Spec |
|---|---|
| Format | .mp3 (primary): maximum browser compatibility |
| Fallback format | .ogg (optional) |
| Sample rate | 44.1 kHz or 48 kHz |
| Bit depth | 16-bit |
| Channels | Mono (L+R identical) preferred |
| Peak normalization | −1.0 dBTP (true peak) |
| Leading silence | None: trimmed to first transient |
| Trailing silence | ≤ 20 ms tail, then trimmed |
| Per-category size budget | ≤ 50 KB total per category |
| Per-file size guideline | ≤ 15 KB per file |
MP3 is preferred because decodeAudioData supports it universally across
modern browsers. WAV files are larger with no Web Audio benefit.