- Waveform progress bar powered by
wavesurfer.js - Playlist with drag-and-drop reorder, repeat, shuffle
- Fully customizable β swap any sub-component, CSS variable theming, light & dark themes
- Accessible β WAI-ARIA patterns, full keyboard navigation, axe-tested
- TypeScript-first β typed props and hooks (
useAudioPlayer, sub-hooks) - SSR-friendly β works with Next.js App Router / Server Components
https://codesandbox.io/p/sandbox/basic-nfrpfq
npm install --save react-modern-audio-player- React 18.0.0 or higher
- react-dom 18.0.0 or higher
For React 16/17 projects, use v1.x of this library.
import AudioPlayer from "react-modern-audio-player";
const playList = [
{
name: "name",
writer: "writer",
img: "image.jpg",
src: "audio.mp3",
id: 1,
},
];
function Player() {
return <AudioPlayer playList={playList} />;
}This library includes the 'use client' directive and can be imported directly in Next.js App Router.
Server Component β render <AudioPlayer> with static props (no hooks, no compound components):
// app/page.tsx β Server Component, no 'use client' needed
import AudioPlayer from "react-modern-audio-player";
const playList = [
{
name: "track",
writer: "artist",
img: "cover.jpg",
src: "audio.mp3",
id: 1,
},
];
export default function Page() {
return <AudioPlayer playList={playList} activeUI={{ playButton: true }} />;
}Client Component β use useAudioPlayer hooks or AudioPlayer.CustomComponent:
"use client";
// app/player/page.tsx β Client Component required for hooks & compound pattern
import AudioPlayer, { useAudioPlayer } from "react-modern-audio-player";
function Controls() {
const { isPlaying, togglePlay } = useAudioPlayer();
return <button onClick={togglePlay}>{isPlaying ? "Pause" : "Play"}</button>;
}
export default function PlayerPage() {
return (
<AudioPlayer playList={playList}>
<AudioPlayer.CustomComponent id="controls">
<Controls />
</AudioPlayer.CustomComponent>
</AudioPlayer>
);
}Why
'use client'? The library's'use client'directive marks the client boundary β it allows Server Components to import and render<AudioPlayer>. However,useAudioPlayer()hooks andAudioPlayer.CustomComponentrequire client-side React features (state, context), so components using them must be Client Components.
| Category | Sections |
|---|---|
| Props | PlayList Β· InitialStates Β· ActiveUI Β· Placement Β· RootContainerProps |
| Override & Style | CustomIcons Β· CoverImgsCss Β· Theme mode Β· ID & Classnames |
| Player Hook API | useAudioPlayer Β· AudioPlayerControls Β· Sub-Hooks |
| Custom Component | Custom Component |
| Accessibility | Keyboard support |
| Example | Example |
interface AudioPlayerProps {
playList: PlayList;
audioInitialState?: InitialStates;
audioRef?: React.MutableRefObject<HTMLAudioElement>;
activeUI?: ActiveUI;
customIcons?: CustomIcons;
coverImgsCss?: CoverImgsCss;
placement?: {
player?: PlayerPlacement;
playList?: PlayListPlacement;
interface?: InterfacePlacement;
volumeSlider?: VolumeSliderPlacement;
};
rootContainerProps?: RootContainerProps;
colorScheme?: "light" | "dark";
}| Prop | Type | Default |
|---|---|---|
playList |
PlayList | [ ] |
audioInitialState |
InitialStates | isPlaying: false repeatType: "ALL" volume: 1 |
activeUI |
ActiveUI | playButton : true |
customIcons |
CustomIcons | undefined |
coverImgsCss |
CoverImgsCss | undefined |
placement |
Placement | playListPlacement : "bottom" interfacePlacement :DefaultInterfacePlacement |
rootContainerProps |
RootContainerProps | width: 100% position: 'static' className: rmap-player-provider |
colorScheme |
"light" | "dark" |
undefined (follows OS prefers-color-scheme) |
type PlayList = Array<AudioData>;
type AudioData = {
src: string;
id: number;
name?: string;
writer?: string;
img?: string;
description?: string | ReactNode;
customTrackInfo?: string | ReactNode;
};Passing playList={[]} renders the player in an empty state without crashing. This is useful while waiting for asynchronous track data:
function App() {
const [tracks, setTracks] = useState<PlayList>([]);
useEffect(() => {
fetchTracks().then(setTracks);
}, []);
// Safe β the player mounts with no audio source and activates once tracks arrive.
return <AudioPlayer playList={tracks} />;
}- When the playlist becomes empty after updates, playback stops and all time state resets.
- When
audioInitialState.curPlayIddoesn't match any track in the current list, the player falls back toplayList[0]automatically.
type InitialStates = Omit<
React.AudioHTMLAttributes<HTMLAudioElement>,
"autoPlay"
> & {
isPlaying?: boolean;
repeatType?: RepeatType;
volume?: number;
currentTime?: number;
duration?: number;
curPlayId: number;
};type ActiveUI = {
all: boolean;
playButton: boolean;
playList: PlayListUI;
prevNnext: boolean;
volume: boolean;
volumeSlider: boolean;
repeatType: boolean;
trackTime: boolean;
trackInfo: boolean;
artwork: boolean;
progress: ProgressUI;
};
type ProgressUI = "waveform" | "bar" | false;
type PlayListUI = "sortable" | "unSortable" | false;type CustomIcons = {
play: ReactNode;
pause: ReactNode;
prev: ReactNode;
next: ReactNode;
repeatOne: ReactNode;
repeatAll: ReactNode;
repeatNone: ReactNode;
repeatShuffle: ReactNode;
volumeFull: ReactNode;
volumeHalf: ReactNode;
volumeMuted: ReactNode;
playList: ReactNode;
};interface CoverImgsCss {
artwork?: React.CSSProperties;
listThumbnail?: React.CSSProperties;
}type PlayerPlacement =
| "bottom"
| "top"
| "bottom-left"
| "bottom-right"
| "top-left"
| "top-right";
type VolumeSliderPlacement = "bottom" | "top" | 'left' | 'right';
type PlayListPlacement = "bottom" | "top";
type InterfacePlacement = {
templateArea?: InterfaceGridTemplateArea;
customComponentsArea?: InterfaceGridCustomComponentsArea<TMaxLength>;
itemCustomArea?: InterfaceGridItemArea;
};
type InterfacePlacementKey =
| Exclude<keyof ActiveUI, "all" | "prevNnext" | "trackTime">
| "trackTimeCurrent"
| "trackTimeDuration";
type InterfacePlacementValue = "row1-1" | "row1-2" | "row1-3" | "row1-4" | ... more ... | "row9-9"
/** if you apply custom components, values must be "row1-1" ~ any more */
type InterfaceGridTemplateArea = Record<InterfacePlacementKey,InterfacePlacementValue>;
type InterfaceGridCustomComponentsArea = Record<componentId,InterfacePlacementValue>;
type InterfaceGridItemArea = Partial<Record<InterfacePlacementKey, string>>;
/** example
* progress : 2-4
* repeatBtn : row1-4 / 2 / row1-4 / 10
*
* check MDN - grid area
* https://developer.mozilla.org/ko/docs/Web/CSS/grid-area
*/const defaultInterfacePlacement = {
templateArea: {
artwork: "row1-1",
trackInfo: "row1-2",
trackTimeCurrent: "row1-3",
trackTimeDuration: "row1-4",
progress: "row1-5",
repeatType: "row1-6",
volume: "row1-7",
playButton: "row1-8",
playList: "row1-9",
},
};rootContainerProps accepts any standard HTMLAttributes<HTMLDivElement> (e.g. className, style, data-*). The root container always has the class rmap-player-provider applied automatically.
β οΈ Setting the native CSScolor-schemeproperty viarootContainerProps={{ style: { colorScheme: "dark" } }}will not toggle the player's theme. The library's theme is driven by theprefers-color-schememedia query and the[data-theme]attribute selector β use the top-levelcolorSchemeprop instead.
Dark mode is driven by
system-theme(prefers-color-scheme: dark) by default. To force a specific theme regardless of OS preference, pass the top-levelcolorScheme="light" | "dark"prop on<AudioPlayer>β this applies adata-themeattribute on.rmap-player-providerwhich overrides the media query. You can override any color by redefining the CSS variables below on.rmap-player-provider.
@media (prefers-color-scheme: dark) {
.rmap-player-provider {
--rm-audio-player-interface-container: #1e1e1e;
/* override other variables as needed */
}
}- rm-audio-player
- rmap-player-provider
.rmap-player-provider {
--rm-audio-player-text-color: #2c2c2c;
--rm-audio-player-shadow: 0 0 0;
--rm-audio-player-interface-container: #f5f5f5;
--rm-audio-player-volume-background: #ccc;
--rm-audio-player-volume-panel-background: #f2f2f2;
--rm-audio-player-volume-panel-border: #ccc;
--rm-audio-player-volume-thumb: #5c5c5c;
--rm-audio-player-volume-fill: rgba(0, 0, 0, 0.5);
--rm-audio-player-volume-track: #ababab;
--rm-audio-player-track-current-time: #0072f5;
--rm-audio-player-track-duration: #8c8c8c;
--rm-audio-player-progress-bar: #0072f5;
--rm-audio-player-progress-bar-background: #393939;
--rm-audio-player-waveform-cursor: #4b4b4b;
--rm-audio-player-waveform-background: var(
--rm-audio-player-progress-bar-background
);
--rm-audio-player-waveform-bar: var(--rm-audio-player-progress-bar);
--rm-audio-player-sortable-list: #eaeaea;
--rm-audio-player-sortable-list-button-active: #0072f5;
--rm-audio-player-selected-list-item-background: #b3b3b3;
}Control the player externally using the useAudioPlayer hook. Must be called inside AudioPlayerStateProvider (or AudioPlayer which wraps it).
import AudioPlayer, { useAudioPlayer } from "react-modern-audio-player";
function PlayerControls() {
const {
isPlaying,
currentTrack,
currentTime,
duration,
togglePlay,
next,
prev,
seek,
setVolume,
setTrack,
} = useAudioPlayer();
return (
<div>
<button onClick={togglePlay}>{isPlaying ? "Pause" : "Play"}</button>
<button onClick={prev}>Prev</button>
<button onClick={next}>Next</button>
<button onClick={() => seek(30)}>+30s</button>
<button onClick={() => setVolume(0.5)}>Volume 50%</button>
<button onClick={() => setTrack(1)}>Track 2</button>
<p>
{currentTrack?.name} β {currentTime.toFixed(0)}s / {duration.toFixed(0)}s
</p>
</div>
);
}
function App() {
const playList = [{ id: 1, src: "audio.mp3", name: "Track 1" }];
return (
<AudioPlayer playList={playList}>
<PlayerControls />
</AudioPlayer>
);
}| Property | Type | Description |
|---|---|---|
isPlaying |
boolean |
Current playback state |
volume |
number |
Current volume (0β1) |
currentTime |
number |
Elapsed time in seconds |
duration |
number |
Track duration in seconds |
repeatType |
RepeatType |
Current repeat mode |
muted |
boolean |
Whether audio is muted |
currentTrack |
AudioData | null |
Currently playing track |
currentIndex |
number |
Index in playlist |
playList |
PlayList |
Full playlist |
play() |
() => void |
Start playback |
pause() |
() => void |
Pause playback |
togglePlay() |
() => void |
Toggle play/pause |
next() |
() => void |
Skip to next track |
prev() |
() => void |
Skip to previous track |
seek(time) |
(time: number) => void |
Seek to time in seconds |
setVolume(vol) |
(volume: number) => void |
Set volume (0β1, clamped) |
toggleMute() |
() => void |
Toggle mute on/off |
setTrack(index) |
(index: number) => void |
Jump to playlist index |
useAudioPlayer subscribes to all context slices. For fine-grained re-render control, use domain-specific sub-hooks:
import {
useAudioPlayerPlayback,
useAudioPlayerTrack,
useAudioPlayerVolume,
useAudioPlayerTime,
useAudioPlayerElement,
} from "react-modern-audio-player";
// Only re-renders on play/pause and repeat type changes
function PlayButton() {
const { isPlaying, togglePlay } = useAudioPlayerPlayback();
return <button onClick={togglePlay}>{isPlaying ? "Pause" : "Play"}</button>;
}
// Only re-renders on time updates
function TimeDisplay() {
const { currentTime, duration } = useAudioPlayerTime();
return (
<span>
{currentTime.toFixed(0)}s / {duration.toFixed(0)}s
</span>
);
}| Hook | Returns |
|---|---|
useAudioPlayerPlayback |
{ isPlaying, repeatType, play, pause, togglePlay } |
useAudioPlayerTrack |
{ currentPlayId, currentIndex, playList, currentTrack, setTrack, next, prev } |
useAudioPlayerVolume |
{ volume, muted, setVolume, toggleMute } |
useAudioPlayerTime |
{ currentTime, duration, seek } |
useAudioPlayerElement |
{ audioEl, waveformInst } (advanced) |
Components inside AudioPlayer can subscribe to only the state slice they need, avoiding unnecessary re-renders.
import {
usePlaybackContext, // curAudioState: { isPlaying, repeatType, volume, muted, isLoadedMetaData }
useTrackContext, // playList, curIdx, curPlayId
useUIContext, // activeUI, interfacePlacement, playListPlacement, playerPlacement, volumeSliderPlacement
useResourceContext, // elementRefs, customIcons, coverImgsCss
} from "react-modern-audio-player";
const MyComponent = () => {
const { curAudioState } = usePlaybackContext();
return <span>{curAudioState.isPlaying ? "Playing" : "Paused"}</span>;
};| Hook | Returns |
|---|---|
usePlaybackContext |
{ curAudioState: AudioState } |
useTrackContext |
{ playList, curIdx, curPlayId } |
useUIContext |
{ activeUI, interfacePlacement, playListPlacement, playerPlacement, volumeSliderPlacement } |
useResourceContext |
{ elementRefs, customIcons, coverImgsCss } |
You can place a custom component anywhere in the player interface using AudioPlayer.CustomComponent. Use useAudioPlayer inside it to access player state and controls.
import AudioPlayer, {
useAudioPlayer,
InterfacePlacement,
} from "react-modern-audio-player";
const activeUI: ActiveUI = {
all: true,
};
const placement = {
interface: {
customComponentsArea: {
playerCustomComponent: "row1-10",
},
} as InterfacePlacement<11>,
/**
* set the generic value of InterfacePlacement to the max interface length + 1
* for correct "row1-10" autocompletion
*/
};
const CustomComponent = () => {
const { currentTime, duration, seek, isPlaying, togglePlay } =
useAudioPlayer();
return (
<>
<button onClick={() => seek(currentTime + 30)}>+30s</button>
<button onClick={togglePlay}>{isPlaying ? "Pause" : "Play"}</button>
<span>
{currentTime.toFixed(0)}s / {duration.toFixed(0)}s
</span>
</>
);
};
<AudioPlayer playList={playList} placement={placement} activeUI={activeUI}>
<AudioPlayer.CustomComponent id="playerCustomComponent">
<CustomComponent />
</AudioPlayer.CustomComponent>
</AudioPlayer>;The player follows WAI-ARIA patterns and is fully navigable by keyboard and screen readers.
All controls are reachable via Tab and respond to standard keyboard activation. The playlist uses the WAI-ARIA "Listbox with Rearrangeable Options" pattern:
| Key | Action |
|---|---|
Tab / Shift+Tab |
Move focus between player controls |
Space / Enter |
Activate the focused button (play/pause, prev/next, repeat, mute, playlist) |
ArrowUp / ArrowDown |
Move focus between playlist items |
Alt+ArrowUp / Alt+ArrowDown |
Reorder the focused playlist item |
Enter / Space on a playlist item |
Select and play that track |
Drag-and-drop reordering is preserved as an alternative β keyboard and mouse both call the same onReorder handler.
function App() {
return (
<div>
<AudioPlayer
playList={playList}
audioInitialState={{
muted: true,
volume: 0.2,
curPlayId: 1,
}}
placement={{
interface: {
templateArea: {
trackTimeDuration: "row1-5",
progress: "row1-4",
playButton: "row1-6",
repeatType: "row1-7",
volume: "row1-8",
},
},
player: "bottom-left",
}}
activeUI={{
all: true,
progress: "waveform",
}}
/>
</div>
);
}





