Installation
pnpm add @hookraft/use-soundHow it works
useSound synthesizes UI sounds entirely in the browser using the Web Audio API — no audio files, no network requests, no external dependencies. Every sound is procedurally generated from oscillators, filters, and gain envelopes tuned to feel natural for UI interactions.
Haptics are layered on top via navigator.vibrate() using pre-mapped patterns that match each sound's intent. Both sound and haptics respect prefers-reduced-motion automatically.
Toast / notification
Fire "notification" when a toast appears and "success" or "error" based on its type. Gives each toast distinct audible identity.
Loading state
"loading" loops automatically until you call stop("loading"). Use isPlaying("loading") to track its state and swap your UI accordingly.
Scroll ticking
Throttle "scroll" so it fires at most once every 80ms during list scroll — any faster and it becomes noise rather than feedback.
Types
type SoundName =
| "click" | "hover" | "scroll"
| "modal-open" | "modal-close"
| "success" | "error" | "warning"
| "toggle-on" | "toggle-off"
| "notification" | "delete"
| "expand" | "collapse"
| "swipe" | "pop"
| "minimize" | "maximize"
| "typing" | "loading" | "complete" | "coin"
type Theme =
| "soft"
| "mechanical"
| "digital"
| "wooden"
| "glass"
type HapticPattern =
| "click" | "double"
| "success" | "error" | "warning"
| "notification"
| "impact-light" | "impact-medium" | "impact-heavy"
| "selection" | "long"
interface Options {
volume?: number
pitch?: "low" | "mid" | "high" | number
speed?: "slow" | "normal" | "fast"
reverb?: boolean
stereo?: number
haptic?: boolean
hapticPattern?: HapticPattern
}
interface GlobalOptions {
globalVolume?: number
muted?: boolean
theme?: Theme
haptics?: boolean
}
interface Return {
play: (name: SoundName, options?: Options) => void
stop: (name?: SoundName) => void
mute: () => void
unmute: () => void
isMuted: boolean
isPlaying: (name: SoundName) => boolean
triggerHaptic: (pattern: HapticPattern) => void
}useSound
import { useSound } from "@hookraft/use-sound"const { play, stop, mute, unmute, isMuted, isPlaying, triggerHaptic } = useSound({
theme: "soft",
globalVolume: 0.3,
haptics: true,
})GlobalOptions
| Prop | Type | Default |
|---|---|---|
theme | useSound.Theme | "soft" |
globalVolume | number | 0.3 |
muted | boolean | false |
haptics | boolean | false |
Return value
| Prop | Type | Default |
|---|---|---|
play | (name: SoundName, options?: Options) => void | - |
stop | (name?: SoundName) => void | - |
mute | () => void | - |
unmute | () => void | - |
isMuted | boolean | - |
isPlaying | (name: SoundName) => boolean | - |
triggerHaptic | (pattern: HapticPattern) => void | - |
Per-call Options
| Prop | Type | Default |
|---|---|---|
volume | number | - |
pitch | "low" | "mid" | "high" | number | - |
speed | "slow" | "normal" | "fast" | - |
reverb | boolean | - |
stereo | number | - |
haptic | boolean | - |
hapticPattern | useSound.HapticPattern | - |
Sound palette
Every sound is tuned for a specific UI context. Each maps automatically to a haptic pattern when haptics are enabled.
| Name | Context | Haptic |
|---|---|---|
"click" | Button press | click |
"hover" | Icon / link mouseenter | impact-light |
"scroll" | List scroll tick | selection |
"modal-open" | Modal / drawer open | impact-medium |
"modal-close" | Modal / drawer close | impact-light |
"success" | Form submit, save confirmed | success |
"error" | Validation failure | error |
"warning" | Alert, caution state | warning |
"toggle-on" | Switch / checkbox on | double |
"toggle-off" | Switch / checkbox off | double |
"notification" | Toast / badge appear | notification |
"delete" | Destructive action | impact-heavy |
"expand" | Accordion / dropdown open | impact-light |
"collapse" | Accordion / dropdown close | impact-light |
"swipe" | Card swipe / drawer drag | impact-medium |
"pop" | Tooltip / popover appear | click |
"minimize" | Panel minimize | impact-light |
"maximize" | Panel maximize | impact-medium |
"typing" | Each keystroke | selection |
"loading" | Spinner / skeleton start (loops) | selection |
"complete" | Loading done | success |
"coin" | Points / reward earned | double |
Themes
Five themes ship out of the box. Each changes the oscillator waveform, filter shape, and envelope timing — making the same sound feel mechanical, glassy, warm, or digital depending on your product's personality.
| Theme | Character | Oscillator |
|---|---|---|
"soft" | Muted, gentle — iOS-like (default) | Sine |
"mechanical" | Keyboard / typewriter clicks | Square |
"digital" | Crisp, synthetic, sci-fi | Sawtooth |
"wooden" | Warm, organic taps | Triangle |
"glass" | Bright, crystalline pings | Sine + peaking filter |
Haptic patterns
Haptic patterns use navigator.vibrate() with tuned duration arrays. They fire automatically alongside sounds when haptics: true, or you can trigger them manually via triggerHaptic().
Haptics are silently skipped when:
navigator.vibrateis unavailable (desktop browsers, iOS Safari)prefers-reduced-motion: reduceis set
| Pattern | Vibration sequence |
|---|---|
"click" | 10ms |
"double" | 10 · 50 · 10ms |
"success" | 15 · 40 · 40ms |
"error" | 30 · 20 · 30 · 20 · 30ms |
"warning" | 20 · 60 · 20ms |
"notification" | 25ms |
"impact-light" | 5ms |
"impact-medium" | 15ms |
"impact-heavy" | 30ms |
"selection" | 8ms |
"long" | 10 · 30 · 10 · 30 · 40ms |