Building a good audio player is harder than it looks. You need:
wavesurf handles all of this out of the box, so you can focus on your actual product.
tsx import { WaveformPlayer } from 'wavesurf';function TrackList({ tracks }) { return (
{tracks.map((track) => ( key={track.id} song={{ id: track.id, title: track.title, artist: track.artist, audioUrl: track.url, duration: track.duration, peaks: track.peaks, // Optional but recommended }} /> ))}
); }That's it. Click play on any track, and the mini-player appears. Click another track, and it seamlessly switches.
---
Architecture & Design Decisions
$3 Problem: In a typical music app, you have multiple track listings, album pages, and a persistent player bar. Without global state, you'd have multiple
elements fighting each other.Solution: wavesurf uses React Context to maintain a single audio source. When you call
play() from anywhere in your app, it:1. Pauses any currently playing audio 2. Loads the new track 3. Starts playback with a volume fade-in 4. Notifies all
WaveformPlayer components to update their UI
`tsx // Any component can control playback const { play, pause, currentSong, isPlaying } = useAudioPlayer();`
$3 Problem: Audio waveforms require decoding audio data and rendering thousands of bars. Doing this poorly kills performance.
Solution: WaveSurfer.js is the industry standard for web audio visualization. It handles:
- Efficient canvas rendering - Audio decoding - Responsive resize handling - Click-to-seek interactions
wavesurf wraps WaveSurfer.js with React lifecycle management, so you don't deal with manual cleanup or memory leaks.
$3 Problem: Decoding audio to generate waveforms is slow—especially for longer tracks or pages with many songs. Users see loading spinners everywhere.
Solution: Generate peaks once (server-side), store them, and pass them to wavesurf:
`tsx song={{ id: '1', title: 'My Song', audioUrl: '/audio/song.mp3', duration: 245, peaks: [0.1, 0.3, 0.5, 0.8, ...], // Pre-computed! }} />`When peaks are provided: - No audio decoding needed — waveform renders instantly - No network request for audio — until the user clicks play - Pages load faster — even with 50+ tracks
#### How to Generate Peaks
Using audiowaveform (recommended):
`bashInstall brew install audiowaveform # macOS apt install audiowaveform # Ubuntu
Generate peaks audiowaveform -i song.mp3 -o peaks.json --pixels-per-second 10 -b 8`Or server-side with FFmpeg/Node.js—compute once when uploading audio, store in your database.
$3 Problem: Clicking play and getting blasted with sudden audio is jarring. Users instinctively reach for the volume.
Solution: wavesurf fades volume from 0 to the user's set level over 3 seconds (configurable). This:
- Creates a professional, polished feel - Prevents startling users - Matches how streaming services behave
`tsx fadeInEnabled: true, // default: true fadeInDuration: 3000, // default: 3000ms }}>`
$3 Problem: Users set their volume, navigate to another page, and it resets.
Solution: Volume is automatically saved to localStorage and restored on page load.
`tsx persistVolume: true, // default: true storageKey: 'myAppVolume', // default: 'audioPlayerVolume' defaultVolume: 0.8, // default: 1 }}>`
$3 Problem: A page with 20 tracks means 20 WaveSurfer instances initializing at once, causing jank.
Solution: wavesurf uses IntersectionObserver to only initialize waveforms when they scroll into view:
`tsx song={song} lazyLoad={true} // default: true />`Tracks off-screen are just empty containers until needed.
$3 Problem: Users want to browse your site while listening. A player embedded in the track list disappears when they navigate.
Solution: The
MiniPlayer component is a fixed bar (bottom or top) that:- Appears when playback starts - Shows current track, progress, volume controls - Has its own mini waveform for seeking - Stays visible during navigation - Can be closed by the user
`tsx position="bottom" // or "top" showVolume={true} // auto-hidden on mobile showClose={true} onClose={() => console.log('Player closed')} />`---
Components
$3 Wraps your app to provide global audio state.
`tsx fadeInEnabled: true, fadeInDuration: 3000, persistVolume: true, storageKey: 'audioPlayerVolume', defaultVolume: 1, onPlay: (song) => analytics.track('play', song), onPause: () => analytics.track('pause'), onEnd: () => analytics.track('songEnded'), onTimeUpdate: (time) => {}, }}> {children}`
$3 Access state and controls from any component:
`tsx const { // State currentSong, // Song | null isPlaying, // boolean currentTime, // number (seconds) duration, // number (seconds) volume, // number (0-1, user's saved volume) displayVolume, // number (0-1, actual volume during fade) isFadingIn, // boolean // Actions play, // (song: Song) => void pause, // () => void togglePlay, // () => void seek, // (time: number) => void setVolume, // (volume: number) => void stop, // () => void } = useAudioPlayer();
`
$3 Displays a track with waveform visualization:
`tsx song={{ id: string, title: string, artist?: string, album?: string, audioUrl: string, duration?: number, peaks?: number[], }} waveformConfig={{ waveColor: '#666666', progressColor: '#D4AF37', cursorColor: '#D4AF37', barWidth: 2, barGap: 1, barRadius: 2, height: 60, }} lazyLoad={true} showTime={true} standalone={false} // Use local audio instead of global context className="" renderHeader={(song, isPlaying) => } renderControls={(song, isPlaying) => } />`#### Standalone Mode
By default,
WaveformPlayer uses the global AudioPlayerProvider context and works with the MiniPlayer. If you want a simpler setup—individual players that don't share state and don't show the mini player bar—use standalone mode:
`tsx // No AudioPlayerProvider needed song={song} standalone={true} />`When to use standalone mode: - Simple pages with just one or two tracks - Embedded players that shouldn't affect the rest of your site - When you don't want the persistent mini player bar
Standalone mode behavior: - Each player manages its own audio element - Clicking play on one song automatically pauses others (even in standalone mode) - No MiniPlayer appears - Volume fade-in and persistence are not applied
$3 Persistent playback bar:
`tsx position="bottom" // 'top' | 'bottom' showVolume={true} showClose={true} onClose={() => {}} className="" waveformConfig={{...}} />`#### Persisting Across Route Changes
To keep the MiniPlayer visible and audio playing while users navigate between pages, place both
AudioPlayerProvider and MiniPlayer in your root layout —not in individual pages.Next.js App Router:
`tsx // app/layout.tsx import { AudioPlayerProvider, MiniPlayer } from 'wavesurf'; import 'wavesurf/styles.css';export default function RootLayout({ children }) { return (
{children} ); }`Next.js Pages Router:
`tsx // pages/_app.tsx import { AudioPlayerProvider, MiniPlayer } from 'wavesurf'; import 'wavesurf/styles.css';export default function MyApp({ Component, pageProps }) { return ( ); }
`React Router:
`tsx // App.tsx import { AudioPlayerProvider, MiniPlayer } from 'wavesurf'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import 'wavesurf/styles.css';function App() { return ( } /> } /> ); }
`Why this works: React Context state persists as long as the provider component stays mounted. By placing it in the root layout, the audio state survives page transitions. If you put the provider inside a page component, it unmounts on navigation and loses the current song.
$3 Social sharing for tracks:
`tsx import { ShareButtons } from 'wavesurf'; url="https://mysite.com/track/123" text="Check out this song!" platforms={['facebook', 'twitter', 'whatsapp', 'copy']} onShare={(platform, url) => analytics.track('share', { platform })} showLabels={false} />
`Available platforms:
facebook, twitter, whatsapp, linkedin, reddit, telegram, email, copy---
Styling
$3
`tsx import 'wavesurf/styles.css';`
$3 Override any of these in your CSS:
`css :root { / Waveform / --wsp-wave-color: #666666; --wsp-progress-color: #D4AF37; --wsp-cursor-color: #D4AF37; / Backgrounds / --wsp-background: transparent; --wsp-background-secondary: rgba(255, 255, 255, 0.05);
/ Buttons / --wsp-button-bg: #D4AF37; --wsp-button-bg-hover: #e5c04a; --wsp-button-text: #000000;
/ Text / --wsp-text: #ffffff; --wsp-text-muted: #a3a3a3;
/ Sizing / --wsp-height: 60px; --wsp-mini-height: 40px; --wsp-button-size: 56px;
/ Mini Player / --wsp-mini-bg: #0a0a0a; --wsp-mini-border-color: #D4AF37; --wsp-mini-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5);
/ Transitions / --wsp-transition: 150ms ease; }
`
$3 All components use BEM-style class names you can target:
-
.wsp-player - WaveformPlayer container - .wsp-play-button - Play/pause button - .wsp-waveform - Waveform container - .wsp-time-display - Time labels - .wsp-mini-player - MiniPlayer container - .wsp-share-buttons - ShareButtons container - .wsp-share-button - Individual share button---
TypeScript All types are exported:
`typescript import type { Song, AudioPlayerState, AudioPlayerActions, AudioPlayerConfig, WaveformConfig, WaveformPlayerProps, MiniPlayerProps, SharePlatform, ShareButtonsProps, } from 'wavesurf';`---
Examples
$3
`tsx function CustomPlayButton({ song }) { const { play, pause, currentSong, isPlaying } = useAudioPlayer(); const isThisSong = currentSong?.id === song.id; const playing = isThisSong && isPlaying; return ( playing ? pause() : play(song)}> {playing ? 'Pause' : 'Play'} ); }
`
$3
`tsx function TrackCard({ track }) { const shareUrl = https://mysite.com/track/${track.id}; return (
url={shareUrl} text={Listen to ${track.title}} platforms={['twitter', 'whatsapp', 'copy']} />
); }`
$3
`tsx onPlay: (song) => { analytics.track('song_play', { songId: song.id, title: song.title, }); }, onEnd: () => { analytics.track('song_completed'); }, }}>``---
Browser Support Requires browsers with: - Web Audio API - CSS Custom Properties - IntersectionObserver
All modern browsers (Chrome, Firefox, Safari, Edge) are supported.
---
License MIT © TheDecipherist