prathna+anthem.mp3) hosted on Cloudflare R2. Because browsers block autoplay without a user gesture, the system gates playback behind an explicit consent step and exposes fade controls for smooth volume transitions.
Architecture overview
AudioProvider
React context that owns the
HTMLAudioElement, consent state, and volume fade logic. Wraps the entire app in layout.tsx.useAudioContext
Hook that reads from
AudioProvider. Use this in any component that needs to control or react to playback.useAudio
Standalone hook that creates its own
HTMLAudioElement for a given src. Use when you need a one-off audio element independent of the global player.AudioPlayer
Floating play/pause button fixed to the bottom-right of the viewport. Adapts its icon colour based on the background brightness at scroll position.
Provider setup
AudioProvider is mounted in the root layout, wrapping ThemeProvider, so every page has access to the audio context.
app/layout.tsx (excerpt)
ThemeProvider is configured with forcedTheme="light". The site does not support dark mode — this prevents system dark-mode preferences from altering the UI.AudioContextType interface
contexts/audio-context.tsx
| Member | Description |
|---|---|
play | Plays audio immediately. Requires hasUserConsent to be true. |
pause | Pauses audio immediately. |
fadeOut(duration?) | Smoothly reduces volume to zero and pauses. Default 1000 ms. |
fadeToVolume(target, duration?) | Interpolates volume to target (0–1). Default 1000 ms. |
toggle | Grants consent if not already granted, then plays or pauses. |
isPlaying | true while audio is playing. |
isLoaded | true once the audio is ready to play (fired by canplaythrough). |
hasUserConsent | true after the user has interacted with the player at least once. |
grantConsent | Marks consent without immediately playing (e.g. after an explicit user gesture). |
Browser autoplay policy
Browsers require a user gesture before audio can play.AudioProvider handles this by:
- Creating the
Audioelement withvolume = 0so it can preload without triggering the autoplay block. - Setting
hasUserConsentReftofalseon mount —play()is a no-op until consent is granted. - The
toggle()method callsgrantConsent()internally, so the first click onAudioPlayerboth grants consent and starts playback.
contexts/audio-context.tsx
Fade effects
Fades are implemented usingsetInterval over 50 steps. This avoids the Web Audio API dependency while still producing smooth transitions.
- fadeOut
- fadeToVolume
Decrements volume by equal steps until it reaches zero, then pauses.
Using useAudioContext in a component
Import useAudioContext wherever you need to read or control global audio:
Standalone useAudio hook
For audio elements independent of the global player (e.g. sound effects or inline media), use the useAudio hook directly:
hooks/use-audio.ts
useAudio returns the same play, pause, fadeOut, and toggle controls as the global context, plus the raw audioRef for advanced use. Unlike AudioContextType, it does not implement a consent gate — call it only in response to a user gesture or after grantConsent() has been called.
AudioPlayer component
The AudioPlayer renders a floating play/pause button. It detects the background colour at the bottom of the visible viewport and switches between light and dark icon styles accordingly.
components/audio-player.tsx
- The button is hidden (
isVisible={false}) until the audio finishes loading (isLoaded). - On first page load (before scrolling), the button plays a
animate-float-attentionCSS animation to attract attention. disabled={!isLoaded}prevents interaction before the audio source is ready.- The
aria-labelupdates dynamically so screen readers announce the correct action.
Audio source
The global background track is loaded from Cloudflare R2:contexts/audio-context.tsx and upload the new file to R2 at the corresponding path.