react-modern-audio-player
v2.3.1
Published
Modern, customizable React audio player with waveform visualization, drag-and-drop playlist, WAI-ARIA accessibility, TypeScript-first API, and Next.js App Router (Server Components) support.
Maintainers
Readme
Highlights
- 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
- Compound slots —
AudioPlayer.Volume,AudioPlayer.Progress,AudioPlayer.PlayList, etc. for partial customization without losing the preset - Multi-instance playlist — render multiple players on the same page with isolated playlist drawers and fully independent audio state
- 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
DEMO
https://codesandbox.io/p/sandbox/basic-nfrpfq
Flexible and Customizable UI
Waveform progress with wavesurfer.js
Customizable layout and placement — with light & dark themes
Full View
Position Change
Particular View
Installation
npm install --save react-modern-audio-playerRequirements
- React 18.0.0 or higher
- react-dom 18.0.0 or higher
For React 16/17 projects, use v1.x of this library.
Quick Start
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} />;
}Next.js / Server Components
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.
Table of Contents
| 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 · Compound Slots | | Accessibility | Keyboard support | | Gotchas | Gotchas | | Example | Example |
Props
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;
speedSelector?: SpeedSelectorPlacement;
};
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) |
PlayList
type PlayList = Array<AudioData>;
type AudioData = {
src: string;
id: number;
name?: string;
writer?: string;
img?: string;
description?: string | ReactNode;
customTrackInfo?: string | ReactNode;
};Empty playlist handling
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.
InitialStates
type InitialStates = Omit<
React.AudioHTMLAttributes<HTMLAudioElement>,
"autoPlay"
> & {
isPlaying?: boolean;
repeatType?: RepeatType;
volume?: number;
currentTime?: number;
duration?: number;
curPlayId: number;
playListExpanded?: boolean;
};
playListExpanded: trueopens the playlist drawer on mount. Consistent with the other fields onaudioInitialState, this is read once at mount and is not tracked in reducer state.
ActiveUI
type ActiveUI = {
all: boolean;
playButton: boolean;
playList: PlayListUI;
prevNnext: boolean;
volume: boolean;
volumeSlider: boolean;
repeatType: boolean;
trackTime: boolean;
trackInfo: boolean;
artwork: boolean;
progress: ProgressUI;
playbackRate: boolean;
};
type ProgressUI = "waveform" | "bar" | false;
type PlayListUI = "sortable" | "unSortable" | false;CustomIcons
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;
};CoverImgsCss
interface CoverImgsCss {
artwork?: React.CSSProperties;
listThumbnail?: React.CSSProperties;
}Placement
type PlayerPlacement =
| "bottom"
| "top"
| "bottom-left"
| "bottom-right"
| "top-left"
| "top-right";
type VolumeSliderPlacement = "bottom" | "top" | "left" | "right";
type SpeedSelectorPlacement = "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 ... | "row10-10"
/** 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
*/Default interface placement
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",
playbackRate: "row1-10",
},
};RootContainerProps
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 CSS
color-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.
Override Style
Theme mode (dark mode)
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 */
}
}ID & Classnames
root ID
- rm-audio-player
Multi-instance note: when multiple
<AudioPlayer>instances share a page the rootidis duplicated across them. Playlist and audio state are still isolated per instance (each player has its own React provider tree and its own<audio>DOM node). If you need per-instance selectors, target via the class names below rather than the id.
root ClassName
- rmap-player-provider
color variables
.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;
}useAudioPlayer
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>
);
}AudioPlayerControls
| 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 |
| playbackRate | number | Current playback rate (1 = normal). Default 1. |
| setPlaybackRate(rate) | (rate: number) => void | Set playback rate. No clamping; browser enforces HTML5 bounds. |
Sub-Hooks
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, playbackRate, play, pause, togglePlay, setPlaybackRate } |
| useAudioPlayerTrack | { currentPlayId, currentIndex, playList, currentTrack, setTrack, next, prev } |
| useAudioPlayerVolume | { volume, muted, setVolume, toggleMute } |
| useAudioPlayerTime | { currentTime, duration, seek } |
| useAudioPlayerElement | { audioEl, waveformInst } (advanced) |
Context Hooks
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 } |
Custom Component
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,
DEFAULT_INTERFACE_GRID_BOUND,
} from "react-modern-audio-player";
const activeUI: ActiveUI = {
all: true,
};
// `as 12` pins the literal type — TS arithmetic widens
// `DEFAULT_INTERFACE_GRID_BOUND + 1` to `number`, which would erase grid-coord
const TOTAL_INTERFACE_GRID_BOUND = (DEFAULT_INTERFACE_GRID_BOUND + 1) as 12;
// `row1-${DEFAULT_INTERFACE_GRID_BOUND}` resolves to `"row1-11"` —
// the first cell beyond the default template area (which fills 1..10).
const customComponentGridArea = `row1-${DEFAULT_INTERFACE_GRID_BOUND}`;
const placement = {
interface: {
customComponentsArea: {
playerCustomComponent: customComponentGridArea,
},
} as InterfacePlacement<typeof TOTAL_INTERFACE_GRID_BOUND>,
/**
* The generic on `InterfacePlacement<N>` is an exclusive upper bound on grid
* indices — usable cells are `1..(N - 1)`. Default is
* `DEFAULT_INTERFACE_GRID_BOUND` (= 11), giving cells `1..10` which the
* built-in template area already occupies (e.g. `playbackRate` at `row1-10`).
* To reserve an additional cell for the custom component, pass `<N + 1>` —
* here `<12>` makes `row1-11` a valid coordinate.
*/
};
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>;Compound Slots
AudioPlayer exposes its built-in controls as static members so you can re-place or augment individual pieces without rebuilding the whole layout.
| Member | Renders |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| AudioPlayer.Progress | progress bar / waveform |
| AudioPlayer.Volume | volume trigger + slider (accepts triggerType?: "click" \| "hover", placement?: VolumeSliderPlacement) |
| AudioPlayer.PlayList | sortable playlist drawer (accepts initialExpanded?) |
| AudioPlayer.PlayListEmpty | fallback rendered inside the playlist drawer when playList is empty |
| AudioPlayer.PlayButton | Play + Prev + Next group (Prev/Next visibility follows activeUI.prevNnext) |
| AudioPlayer.RepeatButton | repeat-type button |
| AudioPlayer.SpeedSelector | playback rate dropdown (accepts options?, formatRate?, triggerType?: "click" \| "hover", placement?: SpeedSelectorPlacement) |
| AudioPlayer.Artwork | track artwork |
| AudioPlayer.TrackInfo | track title / writer |
| AudioPlayer.TrackTime | current + duration time |
| AudioPlayer.CustomComponent | user-defined slot |
Each slot accepts the full GridItemLayoutProps set — gridArea?, visible?, width?, padding?, justifySelf?, UNSAFE_className? — plus its own domain props. AudioPlayer.TrackTime is the exception: it only exposes visible? because the slot maps to two grid areas internally.
Native HTML attributes (className, style, onClick, data-*, etc.) are not forwarded by compound slots. Compose the underlying primitives (PlayBtn, PrevBtn, NextBtn, etc., still exported) when full DOM control is needed; headless support with native attribute pass-through is planned for v3.
Mental model — activeUI vs compound children
activeUIgoverns the preset (default layout) — which built-in controls are shown.- Compound children are explicit placements that always render (
visibledefaults totrue).
The two layers are orthogonal. Compound children render additively alongside the preset. To truly replace a preset control, disable it in activeUI and render the compound counterpart:
// Remove the default volume, re-place it with a custom gridArea
<AudioPlayer playList={playList} activeUI={{ all: true, volume: false }}>
<AudioPlayer.Volume gridArea="row1-5" />
</AudioPlayer>In development, a console.warn is emitted when a compound slot is rendered while its preset counterpart is still active, so silent duplication is easy to catch.
AudioPlayer.PlayButton and activeUI.prevNnext
AudioPlayer.PlayButton is a single slot that renders the Play + Prev + Next group as one unit. There is no separate AudioPlayer.PrevButton / AudioPlayer.NextButton compound slot — Prev / Next are part of the PlayButton compound, and there are two ways to render them.
Method 1 — AudioPlayer.PlayButton compound (group)
Place the whole group at a custom grid area; Prev / Next visibility inside the group follows activeUI.prevNnext (which defaults to activeUI.all):
// Re-place the full Play + Prev + Next group at a custom area
<AudioPlayer
playList={playList}
activeUI={{ all: true, playButton: false }} // hide the preset transport
>
<AudioPlayer.PlayButton gridArea="row1-3" />
</AudioPlayer>
// Same compound, but render Play only — Prev / Next hidden by activeUI.prevNnext
<AudioPlayer
playList={playList}
activeUI={{ all: true, playButton: false, prevNnext: false }}
>
<AudioPlayer.PlayButton gridArea="row1-3" />
</AudioPlayer>Method 2 — primitives via AudioPlayer.CustomComponent
When you need finer control — placing Prev / Play / Next in different grid cells, applying custom wrappers, or attaching native HTML attributes — drop down to the PlayBtn, PrevBtn, and NextBtn primitives (still exported) and render them inside AudioPlayer.CustomComponent:
import AudioPlayer, {
PrevBtn,
PlayBtn,
NextBtn,
} from "react-modern-audio-player";
<AudioPlayer
playList={playList}
activeUI={{ all: true, playButton: false }} // hide the preset transport
placement={{
interface: { customComponentsArea: { "custom-transport": "row1-3" } },
}}
>
<AudioPlayer.CustomComponent id="custom-transport">
<div className="my-transport">
<PrevBtn isVisible />
<PlayBtn />
<NextBtn isVisible />
</div>
</AudioPlayer.CustomComponent>
</AudioPlayer>;PrevBtn / NextBtn accept an isVisible boolean for symmetry with the compound group's visibility logic; pass false to omit them. Use Method 2 when Method 1's group placement and activeUI.prevNnext toggle aren't enough.
Custom empty-playlist UI
Pass children to AudioPlayer.PlayListEmpty to render a custom node inside the playlist drawer when playList is empty. Omit the slot to keep the default (drawer content renders nothing).
<AudioPlayer playList={[]}>
<AudioPlayer.PlayListEmpty>
<div className="my-empty">Playlist is empty</div>
</AudioPlayer.PlayListEmpty>
</AudioPlayer>PlayListEmpty is a marker slot; its children are consumed by the drawer via context and the component itself does not render in-place.
Accessibility
The player follows WAI-ARIA patterns and is fully navigable by keyboard and screen readers.
Keyboard support
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.
Gotchas
Common integration mistakes to avoid:
- Don't toggle the theme via
rootContainerProps.style.colorScheme. The native CSScolor-schemeproperty does not switch the player's theme. Use the top-levelcolorSchemeprop, which drives the[data-theme]attribute and re-initializes the waveform colors. - Set the
InterfacePlacementgeneric when placingcustomComponentsAreabeyond row 9. TypeScript rejects values past the default range, so useInterfacePlacement<N>whereNis(max row length + 1)— e.g.InterfacePlacement<11>for"row1-10"(see Custom Component). AudioPlayer.CustomComponentaccepts a single React element child. It usesReact.cloneElementinternally, so passing multiple children or a primitive (string, number) will throw.- Volume is
0..1, not0..100.setVolumeclamps out-of-range values, sosetVolume(50)silently becomessetVolume(1). - Compound slots don't forward native HTML attributes.
<AudioPlayer.Volume className="...">is rejected by TypeScript — onlyGridItemLayoutProps(layout) pass through. Compose the underlying primitives (PlayBtn,PrevBtn,NextBtn, etc., still exported) when you needclassName,style,onClick, ordata-*. Full headless support is planned for v3. id: 0is a valid track id. The reducer uses nullish checks, so tracks withid: 0are handled correctly — don't filter them out ofplayListon the assumption that zero is falsy.- Don't import the CSS manually. Styles are auto-injected via
sideEffects: ["*.css"];import "react-modern-audio-player/dist/index.css"will 404 or double-load. - Multiple mounted
<AudioPlayer>instances don't share React state, but they do share the user's speakers. Each instance has its own provider and its own<audio>element, so the state is isolated — but if two instances both play, the user hears both tracks simultaneously. Coordinate playback yourself (e.g. pause the others when oneplay()fires).
Example
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>
);
}