Project overview, philosophy, architecture layers, and non-goals of sensory-ui.
sensory-ui is a semantic, modular, opt-in sound layer for React and Next.js apps, distributed as a shadcn/ui registry package. It adds meaningful, non-intrusive sound feedback to UI interactions. It is not a sound effects library, not a music player, not a game audio engine.
Target platform: Next.js (App Router and Pages Router) + React Distribution: shadcn/ui registry (CLI installable) Runtime footprint: Minimal. Built-in audio is synthesized programmatically via the Web Audio API. No public/ assets, no base64 blobs for built-in packs. No global side effects, no forced re-renders.
| Principle | What It Means in Practice |
|---|---|
| Semantic, not decorative | Every sound maps to a meaningful interaction type (activation, navigation, system feedback). No random audio. |
| Explicit opt-in only | No component ever makes a sound unless you pass a sound prop to it. Nothing is automatic. |
| Accessibility-first | Respects prefers-reduced-motion, provides a global kill-switch, and never forces audio on users. |
| Minimal runtime footprint | Zero npm dependencies. The engine (~2 KB gzipped) and full library (~28 KB gzipped) use only native Web Audio API. |
| Registry-native | Built specifically for the shadcn/ui distribution model. One install command, then it lives inside your project like any other component. |
| Composable | Works with any combination of Radix primitives, any shadcn/ui component, or raw HTML elements via the usePlaySound hook. |
sensory-ui is composed of four runtime layers. Sound travels one path: a user event becomes a command, the command passes policy, resolves to a handler, and the engine plays the effect:
| Layer | File | Responsibility |
|---|---|---|
| Provider | config/provider.tsx | React context, playSound dispatch, reduced-motion check, AudioContext lifecycle |
| Component | button.tsx, dialog.tsx, etc. | Accepts sound prop, calls playSound on interaction events |
| Engine | config/engine.ts | Web Audio API singleton, buffer decode/cache, playback scheduling |
| Config | config/config.ts, config/registry.ts | Role resolution, pack lookup, volume, enable/disable |
The distribution layer (shadcn registry) is separate from runtime and handles how files are installed into your project.
Everything lives inside a single well-scoped folder:
components/ui/sensory-ui/
config/ ← internal engine & config layer
engine.ts ← Web Audio engine (core)
provider.tsx ← SensoryUIProvider (React context)
config.ts ← runtime config + mergeConfig + resolveRole
sound-roles.ts ← TypeScript types for all SoundRole values
registry.ts ← role → SoundPack mapping
use-play-sound.ts ← usePlaySound({ sound, volume }) hook
sounds/ ← audio synthesizers as TypeScript modules
core/ ← instrument-based sound generation
tunes.ts ← base tune definitions (frequencies, durations)
instruments.ts ← 9 instrument configurations
factory.ts ← tune + instrument → synthesizer function
effects.ts ← reverb, delay, chorus, distortion chain
pack-generator.ts ← generates full packs from instruments
index.ts ← re-exports core modules
packs.ts ← all 9 sound packs
index.ts ← main entry point for sounds
button.tsx ← patched shadcn Button with sound prop
dialog.tsx
tabs.tsx
...Built-in sounds are synthesized programmatically via the Web Audio API. No
files in public/sounds/, no base64 blobs, no network requests for built-in
packs. Custom role overrides may load audio from URLs or base64 data URIs.
// app/layout.tsx
import { SensoryUIProvider } from "@/components/ui/sensory-ui/config/provider";
export default function RootLayout({ children }) {
return (
<html>
<body>
<SensoryUIProvider>{children}</SensoryUIProvider>
</body>
</html>
);
}// Any component
import { Button } from "@/components/ui/sensory-ui/button";
export function SaveButton() {
return (
<Button sound="interaction.tap" onClick={handleSave}>
Save
</Button>
);
}These are explicitly out of scope for the initial version:
sound prop