Overview
The useSound hook is the primary way to play sounds in React applications. It provides fine-grained control over playback with options for volume, speed, interruption, and lifecycle callbacks.
Installation
After adding a sound with the CLI, import and use it:
import { useSound } from "@/hooks/use-sound" ;
import clickSound from "@/sounds/click-elegant" ;
export function Button () {
const [ play ] = useSound ( clickSound );
return < button onClick = { play } > Click me </ button > ;
}
Basic Usage
Import the Hook
import { useSound } from "@/hooks/use-sound" ;
Import Your Sound
import successSound from "@/sounds/success-chime" ;
Call the Hook
const [ play , { stop , pause , isPlaying , duration , sound }] = useSound ( successSound );
Play the Sound
< button onClick = { play } > Play Success Sound </ button >
API Reference
Hook Signature
function useSound (
sound : SoundAsset ,
options ?: UseSoundOptions
) : UseSoundReturn
SoundAsset Type
Each sound you import is a SoundAsset:
interface SoundAsset {
name : string ; // Unique identifier
dataUri : string ; // Base64-encoded audio data
duration : number ; // Duration in seconds
format : "mp3" | "wav" | "ogg" ;
license : "CC0" | "OGA-BY" | "MIT" ;
author : string ; // Original creator
}
Return Value
The hook returns a tuple:
type UseSoundReturn = readonly [
play : PlayFunction ,
controls : SoundControls
]
play
stop
pause
isPlaying
duration
sound
Function to start playback. Accepts optional overrides: type PlayFunction = ( overrides ?: {
volume ?: number ;
playbackRate ?: number ;
}) => void ;
Stops playback immediately and resets position Pauses playback (calls stop internally) Boolean state indicating if sound is currently playing Sound duration in seconds, or null if not yet loaded The original SoundAsset object passed to the hook
Options
Volume
Control playback volume from 0 (silent) to 1 (full volume):
const [ play ] = useSound ( clickSound , {
volume: 0.5 // 50% volume
});
Volume level from 0 to 1. Changes are reactive - updating this value will adjust volume during playback.
Static Volume
Dynamic Volume
Per-Call Override
const [ play ] = useSound ( sound , { volume: 0.3 });
Playback Rate
Adjust playback speed (pitch and tempo):
const [ play ] = useSound ( sound , {
playbackRate: 1.5 // 1.5x speed
});
Speed multiplier. Values < 1 slow down, > 1 speed up. Typical range: 0.5 to 2.0.
Slow Motion
Fast Forward
Variable Speed
const [ play ] = useSound ( sound , { playbackRate: 0.5 });
Interrupt
Control whether new playback stops current playback:
const [ play ] = useSound ( sound , {
interrupt: true // Stop current playback before starting new
});
If true, calling play() stops any currently playing instance first. If false, sounds can overlap.
Use interrupt: true for :
UI sounds that shouldn’t overlap (button clicks)
Status sounds (notifications, alerts)
Voice clips or announcements
Use interrupt: false for :
Ambient effects that can layer
Musical notes or chords
Multiple simultaneous impacts
Non-Interrupting (Default)
Interrupting
const [ play ] = useSound ( laserSound );
// Rapid clicks create overlapping sounds
< button onClick = { play } > Pew Pew Pew </ button >
Sound Enabled
Globally enable/disable sound playback:
const [ soundEnabled , setSoundEnabled ] = useState ( true );
const [ play ] = useSound ( sound , { soundEnabled });
Master switch for playback. When false, calling play() does nothing. Perfect for user preferences.
This option is checked every time play() is called. If soundEnabled is false, the function returns immediately without creating audio nodes.
User Preference
Context-Based
function useUserSoundPreference () {
const [ enabled , setEnabled ] = useState (() => {
return localStorage . getItem ( "soundEnabled" ) !== "false" ;
});
const toggle = () => {
const newValue = ! enabled ;
setEnabled ( newValue );
localStorage . setItem ( "soundEnabled" , String ( newValue ));
};
return [ enabled , toggle ] as const ;
}
function MyComponent () {
const [ soundEnabled ] = useUserSoundPreference ();
const [ play ] = useSound ( clickSound , { soundEnabled });
return < button onClick = { play } > Click </ button > ;
}
Lifecycle Callbacks
React to playback events:
const [ play ] = useSound ( sound , {
onPlay : () => console . log ( "Started" ),
onEnd : () => console . log ( "Finished naturally" ),
onPause : () => console . log ( "Paused" ),
onStop : () => console . log ( "Stopped" )
});
Called when play() successfully starts playback
Called when sound finishes playing naturally (not stopped manually)
Called when pause() is called
Called when stop() is called or when interrupted
Analytics
UI State Updates
Chaining Sounds
const [ play ] = useSound ( sound , {
onPlay : () => {
analytics . track ( "Sound Played" , { sound: sound . name });
},
onEnd : () => {
analytics . track ( "Sound Completed" , { sound: sound . name });
}
});
Advanced Patterns
Play with Overrides
Override volume or playback rate for specific calls:
const [ play ] = useSound ( sound , {
volume: 0.7 ,
playbackRate: 1.0
});
// Use default settings
play ();
// Override for this playback only
play ({ volume: 0.3 , playbackRate: 0.5 });
// Original settings remain for next play()
play ();
Overrides don’t change the hook’s default options - they only apply to that specific play() call.
Conditional Playback
function NotificationButton () {
const [ play ] = useSound ( notificationSound );
const [ hasPermission , setHasPermission ] = useState ( false );
const handleClick = () => {
if ( hasPermission ) {
play ();
}
// Handle notification logic
};
return < button onClick = { handleClick } > Notify </ button > ;
}
Multiple Sounds
function GameButton () {
const [ playHover ] = useSound ( hoverSound , { volume: 0.3 });
const [ playClick ] = useSound ( clickSound , { volume: 0.5 });
const [ playError ] = useSound ( errorSound , { volume: 0.7 });
const handleClick = async () => {
try {
playClick ();
await submitForm ();
} catch ( error ) {
playError ();
}
};
return (
< button
onMouseEnter = { playHover }
onClick = { handleClick }
>
Submit
</ button >
);
}
Progress Tracking
function SoundPlayer () {
const [ play , { isPlaying , duration , stop }] = useSound ( sound );
const [ elapsed , setElapsed ] = useState ( 0 );
useEffect (() => {
if ( ! isPlaying ) return ;
const interval = setInterval (() => {
setElapsed ( prev => {
const next = prev + 0.1 ;
if ( duration && next >= duration ) {
clearInterval ( interval );
return duration ;
}
return next ;
});
}, 100 );
return () => clearInterval ( interval );
}, [ isPlaying , duration ]);
const progress = duration ? ( elapsed / duration ) * 100 : 0 ;
return (
< div >
< button onClick = { play } disabled = { isPlaying } > Play </ button >
< button onClick = { stop } disabled = { ! isPlaying } > Stop </ button >
< div className = "progress-bar" >
< div style = { { width: ` ${ progress } %` } } />
</ div >
< span > { elapsed . toFixed ( 1 ) } s / { duration ?. toFixed ( 1 ) } s </ span >
</ div >
);
}
The Web Audio API doesn’t expose playback position directly. For accurate progress, implement timing logic as shown above.
Reactive Volume
Volume changes apply immediately during playback:
function VolumeControl () {
const [ volume , setVolume ] = useState ( 1 );
const [ play , { isPlaying }] = useSound ( sound , { volume });
return (
< div >
< button onClick = { play } > Play </ button >
< input
type = "range"
min = "0"
max = "1"
step = "0.01"
value = { volume }
onChange = { ( e ) => setVolume ( Number ( e . target . value )) }
/>
< span > { Math . round ( volume * 100 ) } % </ span >
</ div >
);
}
Implementation Details
Audio Context
The hook uses the Web Audio API via a shared audio context:
import { getAudioContext } from "@/lib/sound-engine" ;
const ctx = getAudioContext ();
// Returns singleton AudioContext instance
Buffer Caching
Sounds are decoded once and cached:
// First render: decode audio data
const buffer = await decodeAudioData ( sound . dataUri );
bufferRef . current = buffer ;
// Subsequent plays: reuse buffer
source . buffer = bufferRef . current ;
Caching happens at both the engine level (all sounds) and component level (per hook instance).
Audio Graph
Each playback creates this audio graph:
AudioBufferSourceNode → GainNode → AudioContext.destination
(buffer) (volume) (speakers)
Cleanup
The hook automatically cleans up on unmount:
useEffect (() => {
return () => {
if ( sourceRef . current ) {
try {
sourceRef . current . stop ();
} catch {
// Already stopped
}
}
};
}, []);
Best Practices
Don’t call play() during render . Only call in event handlers or effects:// ❌ Bad
const [ play ] = useSound ( sound );
play (); // Called during render!
// ✅ Good
const [ play ] = useSound ( sound );
useEffect (() => play (), []); // Called after mount
Use interrupt for UI sounds : Set interrupt: true for button clicks and UI feedback to prevent overlapping.
Respect user preferences : Always provide a way to disable sounds and respect prefers-reduced-motion.
Volume ranges : Keep UI sounds between 0.3-0.7 volume. Full volume (1.0) can be jarring.
Troubleshooting
Check that soundEnabled is true
Verify the sound imported correctly
Ensure play() is called in an event handler (not during render)
Check browser console for Web Audio API errors
Verify AudioContext isn’t blocked by browser autoplay policy
Sound plays multiple times
Set interrupt: true to stop previous playback before starting new playback.
Volume changes don't work
Volume is reactive and updates during playback. Make sure you’re passing a state variable, not a constant.
The hook cleans up automatically. If you see warnings, ensure you’re not calling play() after unmount.
Next Steps