v2.3.0
v2.3.0 (2026-04-30)
β¨ New Features
-
Playback speed support (Closes #3): the player now supports per-track playback speed via three new public surfaces.
-
1.
AudioPlayer.SpeedSelectorcompound slot β a Dropdown-based UI that displays the current rate as a clickable label (e.g.1Γ,1.5Γ) and opens a menu of selectable rates. Defaults to[0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]. Mounts automatically alongside the preset whenactiveUI.playbackRateistrue(oractiveUI.allistrue); acceptsoptions?: number[]andformatRate?: (rate: number) => stringfor customization.<AudioPlayer playList={list} activeUI={{ all: true, playbackRate: true }} /> // or as a compound child with custom rate options <AudioPlayer playList={list} activeUI={{ all: true, playbackRate: false }}> <AudioPlayer.SpeedSelector options={[1, 1.5, 2, 3]} formatRate={(r) => `${r}x`} gridArea="row1-11" /> </AudioPlayer>
The dropdown menu uses
role="menu"+role="menuitemradio"witharia-checkedreflecting the active rate (WAI-ARIA APG menu pattern, matches video.js / Vidstack). -
2.
useAudioPlayer().playbackRate+setPlaybackRate(rate)β the imperative API exposes the current rate and a setter. Available on the facade and onuseAudioPlayerPlaybackfor fine-grained subscriptions.const { playbackRate, setPlaybackRate } = useAudioPlayer(); // ... later setPlaybackRate(1.5);
-
3.
audioInitialState.playbackRateβ initial rate at mount, defaults to1. No clamping is applied at any layer; the browser enforces HTML5playbackRatebounds.
Implementation notes: the new
SET_PLAYBACK_RATEreducer action is the single write path;useAudiomirrors the rate toaudioEl.playbackRatevia a sync effect plus a re-apply insideonLoadedMetadata(the browser resets the DOMplaybackRateto1onsrcchange, mirroring the existingvolumere-apply pattern).DEFAULT_INTERFACE_GRID_BOUND(renamed fromdefaultInterfacePlacementMaxLengthin this release β see Breaking Changes) was bumped from10to11to accommodateplaybackRate: "row1-10"in the default template area; consumers passing an explicitinterfacePlacementlength parameter are unaffected. -
-
Volume and SpeedSelector dropdown customization: both compound slots now expose
triggerType?: "click" \| "hover"and aplacement?prop typed as the per-slot domain alias βVolumeSliderPlacementfor<AudioPlayer.Volume>andSpeedSelectorPlacementfor<AudioPlayer.SpeedSelector>(both currently resolve to"top" \| "bottom" \| "left" \| "right").AudioPlayer.SpeedSelectoralso gains a top-levelplacement.speedSelector?: SpeedSelectorPlacementprovider option that mirrors the existingplacement.volumeSlider. Resolution order (both knobs, both components): compound prop > UIContext > component default. Defaults are unchanged βVolumestays ontriggerType="hover"with viewport-aware auto-placement,SpeedSelectorstays ontriggerType="click"withplacement="top".// Switch Volume to a click-opened panel and force the menu below the trigger <AudioPlayer.Volume triggerType="click" placement="bottom" /> // Per-instance SpeedSelector placement <AudioPlayer.SpeedSelector placement="bottom" triggerType="hover" /> // Or set a default for every SpeedSelector mounted under this provider <AudioPlayer playList={list} placement={{ speedSelector: "bottom" }} />
Implementation note: when
Volumeresolves totriggerType="click", the inner<Dropdown.Content>roleswitches from"tooltip"to"dialog"for semantic correctness βtooltipsemantics are reserved for non-interactive informational popovers and don't fit a click-opened slider panel. SpeedSelector keepsrole="menu"regardless oftriggerType(the menu pattern allows either trigger style).
π₯ Breaking Changes
-
row1-10grid slot now occupied by defaultplaybackRatecontrol: The new defaultplaybackRateplacement atrow1-10will collide with any existingcustomComponentsAreaortemplateAreaentry that targets the same cell. Two ways to resolve:- Move the conflicting custom area to a different cell (e.g.
row1-11) - Disable the new slot by setting
activeUI={{ ..., playbackRate: false }}
No collision warning is emitted at runtime β the symptom is overlapping cells in the rendered grid.
- Move the conflicting custom area to a different cell (e.g.
-
defaultInterfacePlacementMaxLengthrenamed toDEFAULT_INTERFACE_GRID_BOUND: the magic-number constant exported from the package barrel is renamed to follow the project's UPPER_SNAKE_CASE convention for enum-like constants and to clarify its semantics. The value (11) and behavior are unchanged β it is the exclusive upper bound on grid indices, so usable cells are1..(DEFAULT_INTERFACE_GRID_BOUND - 1)(i.e.1..10by default). Consumers importing the old name must update their import:- import { defaultInterfacePlacementMaxLength } from "react-modern-audio-player"; + import { DEFAULT_INTERFACE_GRID_BOUND } from "react-modern-audio-player";
-
AudioPlayer.Volumerole attribute change withtriggerType="click": WhenVolumeresolves totriggerType="click"(via the compound prop, theplacement.volumeSliderprovider option, or direct usage), the inner<Dropdown.Content>roleswitches from"tooltip"to"dialog". Consumers assertingrole="tooltip"in tests, targeting[role="tooltip"]in CSS, or relying on tooltip semantics in accessibility tooling must update those expectations. Hover mode (the default) is unchanged.
π Bug Fixes
-
Auto-play after track swap on bar-progress players: pressing next (or letting a track end) while playing now correctly continues into the new track when
activeUI.progress === "bar". Previously, only waveform-mounted players auto-played the next track βuseWavesurfer'sonReadycallback was the only path callingaudioEl.play()after asrcchange, so bar-only players (and waveform players that had never been mounted) loaded the new track and sat silent while the play button still showed the "playing" icon. Fix moves the responsibility intouseAudio's play effect by addingaudioResetKeyto its deps; the wavesurferonReadyhandler no longer callsplay(), removing the duplicate path. -
PREV button silent on bar-progress players when crossing tracks: the same root cause manifested on the previous-track path. The
PREV_AUDIOreducer's track-change branches (SHUFFLE and the default infinite-loop wrap) used to skip theaudioResetKeybump, so the play effect's new key-based dep wouldn't fire on those branches either. Both branches now bumpaudioResetKeyto matchNEXT_AUDIO, restoring playback continuity across all repeat modes.
Full Changelog: v2.2.0...v2.3.0