Setting up a floating music player in React Native

This blogpost will walk you through the process of creating a basic music player in React Native as well as adding a floating player component to keep track of the song's progress across all screens of your app.
The two main libraries we will use are react-native-track-player and react-navigation.
- Setup your app
- Implement basic routing
- Setup the music player
- Create the Player modal
- Create the Home screen
- Implement the floating player
Setup your app
Generate an app, my example uses React Native 0.70.6
and React 18.1.0
:
npx react-native init RNFloatingPlayer --template react-native-template-typescript
Install the necessary dependencies:
yarn add react-native-track-player @react-native-community/slider @react-navigation/native react-native-screens react-native-safe-area-context @react-navigation/native-stack @react-navigation/bottom-tabs
Additional steps for react-navigation:
For Android you need to add the following inside: android/app/src/main/java/com/rnfloatingplayer/MainActivity.java
:
package com.rnfloatingplayer;
import android.os.Bundle; // <- this import
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
public class MainActivity extends ReactActivity {
// this method inside MainActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
}
Install pods for iOS:
(cd ios && pod install)
We're all setup, let's code!
Implement basic routing
The goal here is to setup the minimum amount of routes to demonstrate the floating player.
We have 1 screen + 1 modal:
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import {Track} from 'react-native-track-player';
export enum Routes {
ROOT = 'root',
PLAYER = 'player',
HOME = 'home',
}
export type RootStackParamList = {
[Routes.ROOT]: undefined;
[Routes.PLAYER]: {index: number; position?: number; queue: Track[]};
[Routes.HOME]: undefined;
};
export type RootStackScreenProps<T extends Routes> = NativeStackScreenProps<
RootStackParamList,
T
>;
// ...
Root
contains the tab stack, Home
will be a tab screen and Player
is a modal.
And two navigators:
- Native stack
- Bottom tab
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {
createNativeStackNavigator,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
// ...
const RootStack = createNativeStackNavigator<RootStackParamList>();
const TabStack = createBottomTabNavigator<RootStackParamList>();
// ...
Create the two navigators where the bottom tab is nested in a native stack:
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {
createNativeStackNavigator,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
// ...
const TabNavigator = () => {
return (
<TabStack.Navigator>
<TabStack.Screen
component={HomeScreen}
name={Routes.HOME}
options={{
headerTitle: 'Home',
title: 'Home',
}}
/>
</TabStack.Navigator>
);
};
export const RootNavigator = () => {
return (
<RootStack.Navigator>
<RootStack.Screen
name={Routes.ROOT}
component={TabNavigator}
options={{headerShown: false}}
/>
<RootStack.Screen
name={Routes.PLAYER}
component={PlayerModal}
options={{presentation: 'modal'}}
/>
</RootStack.Navigator>
);
};
Wrap the app entrypoint with our new navigator:
import {NavigationContainer} from '@react-navigation/native';
const App = () => {
return (
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
);
};
Setup the music player
We will prepare all of the utilities required to run our music player.
First create a custom hook to initialise our player:
import TrackPlayer from 'react-native-track-player';
export const useInitPlayer = () => {
useEffect(() => {
TrackPlayer.setupPlayer();
return () => {
TrackPlayer.reset();
};
}, []);
};
// ...
Use this in the app's entrypoint:
import {NavigationContainer} from '@react-navigation/native';
const App = () => {
useInitPlayer();
return (
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
);
};
Create a hook to handle player controls and state:
import TrackPlayer, {
State,
Track
} from 'react-native-track-player';
// ...
export type Controls = {
position: number;
isPlaying: boolean;
duration: number;
startTrack: () => Promise<void>;
skipToNextTrack: () => Promise<void>;
skipToPreviousTrack: () => Promise<void>;
};
export type UsePlayerControlsResponse = {
currentTrack?: Track;
currentTrackIndex?: number;
setCurrentTrack: Dispatch<SetStateAction<Track | undefined>>;
controls: Controls;
};
export const usePlayerControls = (): UsePlayerControlsResponse => {
const [playerState, setPlayerState] = useState<State>();
const [currentTrack, setCurrentTrack] = useState<Track>();
const [currentTrackIndex, setCurrentTrackIndex] = useState<number>();
// ...
playerState
is needed to determine the isPlaying
state and change the play-pause button state.
duration
is the total length of a song used for the progress bar of a song.
position
is the current progress in seconds of a song, which is also used for the progress bar.
startTrack
skipToNextTrack
skipToPreviousTrack
are handler for the player's control buttons (respectively play-pause, skip to next, skip to previous).
currentTrack
setCurrentTrack
are used to keep track of the current song playing.
currentTrackIndex
setCurrentTrackIndex
are used to keep track of the position of the current song in a queue.
We need to listen to changes in specific events to update our states with useTrackPlayerEvents
a hook from react-native-track-player
:
import TrackPlayer, {
Event,
State,
Track,
useTrackPlayerEvents,
} from 'react-native-track-player';
// ...
export const usePlayerControls = (): UsePlayerControlsResponse => {
// ...
useTrackPlayerEvents(
[Event.PlaybackTrackChanged, Event.PlaybackState],
async event => {
if (
event.type === Event.PlaybackTrackChanged &&
event.nextTrack != null
) {
const track = await TrackPlayer.getTrack(event.nextTrack);
if (track) {
if (track.url !== currentTrack?.url) {
setCurrentTrack(track);
}
if (currentTrackIndex !== event.nextTrack) {
setCurrentTrackIndex(event.nextTrack);
}
}
}
if (event.type === Event.PlaybackState) {
setPlayerState(event.state);
}
},
);
// ...
Setup the player control handlers and get the progress of the current song playing with useProgress
, a hook from react-native-track-player
:
import TrackPlayer, {
Event,
State,
Track,
useProgress,
useTrackPlayerEvents,
} from 'react-native-track-player';
// ...
export const usePlayerControls = (): UsePlayerControlsResponse => {
// ...
const {position, duration} = useProgress();
const skipToNextTrack = () => TrackPlayer.skipToNext();
const skipToPreviousTrack = () => TrackPlayer.skipToPrevious();
const startTrack = async () => {
const state = await TrackPlayer.getState();
if (state !== State.Playing) {
await TrackPlayer.play();
} else {
await TrackPlayer.pause();
}
};
return {
controls: {
startTrack,
skipToNextTrack,
skipToPreviousTrack,
duration: duration || 25,
isPlaying:
playerState !== State.Playing && playerState !== State.Buffering,
position,
},
currentTrack,
currentTrackIndex,
setCurrentTrack,
};
};
Create the Player modal
This modal takes 3 parameters: the queue
of songs to play, the index
of the initial song to play and the position
of the initial song to play.
export const PlayerModal: FC<RootStackScreenProps<Routes.PLAYER>> = ({
route: {
params: {position = 0, index, queue},
},
}) => {
// ...
We will handle creating the queue in the player and changing the initial song's progress (position
) and its position in the queue (index
):
import TrackPlayer from 'react-native-track-player';
export const PlayerModal: FC<RootStackScreenProps<Routes.PLAYER>> = ({
route: {
params: {position = 0, index, queue},
},
}) => {
useEffect(() => {
const handleQueue = async () => {
await TrackPlayer.add(queue);
if (index >= 0) {
await TrackPlayer.skip(index);
}
if (position && position > 0) {
await TrackPlayer.seekTo(position);
}
};
handleQueue();
}, [index, queue, position]);
// ...
Get the current song to play, its progress and display it:
import TrackPlayer from 'react-native-track-player';
export const PlayerModal: FC<RootStackScreenProps<Routes.PLAYER>> = ({
route: {
params: {position = 0, index, queue},
},
}) => {
// ...
const {controls, currentTrack} = usePlayerControls();
if (!currentTrack) {
return <ActivityIndicator style={styles.centered_horizontal} />;
}
return (
<View style={styles.centered_horizontal}>
<Text>
{currentTrack.title} - {currentTrack.artist}
</Text>
{currentTrack.artwork && typeof currentTrack.artwork === 'string' && (
<Image
resizeMode="cover"
style={styles.image_dimensions}
source={{uri: currentTrack.artwork}}
/>
)}
<Controls {...controls} />
</View>
);
};
Here is the <Controls />
component, which takes the handlers defined in player.utils.ts
:
import Slider from '@react-native-community/slider';
import {Controls as ControlProps} from '../player.utils';
export const Controls: FC<ControlProps> = ({
startTrack,
skipToNextTrack,
skipToPreviousTrack,
duration,
position,
isPlaying,
}) => {
return (
<View>
<Slider maximumValue={duration} minimumValue={0} value={position} />
<View style={styles.row_spaced_evenly}>
<Button title="prev" onPress={skipToPreviousTrack} />
<Button title={isPlaying ? 'play' : 'pause'} onPress={startTrack} />
<Button title="next" onPress={skipToNextTrack} />
</View>
</View>
);
};
Here's the result:

Create the Home screen
This screen will just display a button to open the Player modal:
export const HomeScreen: FC<RootStackScreenProps<Routes.HOME>> = ({
navigation: {navigate},
}) => {
const openPlayer = () => navigate(Routes.PLAYER, {index: 0, queue: songs});
return (
<View style={styles.centered_horizontal}>
<Button title="Open player" onPress={openPlayer} />
</View>
);
};
This button launches the Player with a list of songs, the first song in the list is played.
For testing purpose songs
is a simple array of tracks fetched from the network:
import {Track} from 'react-native-track-player';
const ARTWORK_BASE_URL = 'https://source.unsplash.com/random/400?animals&sig=';
const SONG_BASE_URL = 'https://bigsoundbank.com/UPLOAD/mp3/';
export const songs: Track[] = Array.from(Array(15)).map((_, index) => ({
album: 'free sound',
artist: 'bigsoundbank',
artwork: `${ARTWORK_BASE_URL}${index}`,
date: new Date().toISOString(),
duration: 26,
title: (index + 1).toString().padStart(4, '0'),
url: `${SONG_BASE_URL}${(index + 1).toString().padStart(4, '0')}.mp3`,
}));
Here's the result:

Implement the floating player
Once the music player is implemented and playable, we will add a floating minimal player component on every screen to keep track of a playing song's progress.
There are multiple ways to do it based on the design you wish to integrate:
- Integrate with react-navigation by modifying a component (header, tap bar, ...)
- Add a component next to the
<NavigationContainer />
and use absolute positioning
In this case we will display the floating player as a thin bar above the bottom tap bar, like you would see in Spotify.
In the routes
file modify the TabNavigator
to add a custom bottom tap-bar for the tab navigator:
const TabNavigator = () => {
return (
<TabStack.Navigator tabBar={CustomBottomTabBar}> // <- here
<TabStack.Screen
component={HomeScreen}
name={Routes.HOME}
options={{
headerTitle: 'Home',
title: 'Home',
}}
/>
</TabStack.Navigator>
);
};
CustomBottomTabBar
is composed of the default react-navigation
tab-bar and a new component FloatingPlayer
that will show the playing song's progress:
import {BottomTabBar, BottomTabBarProps} from '@react-navigation/bottom-tabs';
export const CustomBottomTabBar = (props: BottomTabBarProps) => (
<View>
<FloatingPlayer />
<BottomTabBar {...props} />
</View>
);
The FloatingPlayer
component simply displays a minimal version of the Player modal:
import Slider from '@react-native-community/slider';
export const FloatingPlayer = () => {
const {navigate} = useNavigation<NavigationProp<RootStackParamList>>();
const {
currentTrackIndex,
currentTrack,
controls: {isPlaying, startTrack, position, duration},
} = usePlayerControls();
if (!currentTrack) {
return null;
}
const {artist, title} = currentTrack;
const playerPressHandler = () =>
navigate(Routes.PLAYER, {
index:
currentTrackIndex && currentTrackIndex >= 0 ? currentTrackIndex : 0,
position: position,
queue: songs,
});
return (
<Pressable onPress={playerPressHandler}>
<View style={styles.container}>
<View style={styles.centered_row_space_between}>
<Text>
{title} - {artist}
</Text>
<Button title={isPlaying ? 'play' : 'pause'} onPress={startTrack} />
</View>
<Slider maximumValue={duration} minimumValue={0} value={position} />
</View>
</Pressable>
);
};
With the usePlayerControls
hook we created, we can get the handlers and state of the current song playing. Then you can modify your floating player as you see fit.
Here we just display the title, artist, song progress and a play-pause button.
Pressing on the FloatingPlayer
component will open the Player modal at the correct song position
and index
.
Here's the result:

And that's it !
Visit this documentation to learn more about react-native-track-player and all of the cool features you can add to your new music player!
Check out our other React Native blogpost