Setting up a floating music player in React Native

A starter's guide on how to set up a floating music player in React Native.
8 min read
Aylin Gokalp
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.


  1. Setup your app
  2. Implement basic routing
  3. Setup the music player
  4. Create the Player modal
  5. Create the Home screen
  6. 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
NPM v8.19.0 - generate a React Native app

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
NPM v8.19.0 - install dependencies

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);
  }
MainActivity.java

Install pods for iOS:

(cd ios && pod install)
NPM v8.19.0 - install pods

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
>;

// ...
routes.tsx - typing

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>();

// ...
routes.tsx - navigation stacks

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>
  );
};
routes.tsx - navigators

Wrap the app entrypoint with our new navigator:

import {NavigationContainer} from '@react-navigation/native';

const App = () => {
  return (
    <NavigationContainer>
      <RootNavigator />
    </NavigationContainer>
  );
};
App.tsx

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();
    };
  }, []);
};

// ...
player.utils.ts - useInitPlayer custom hook

Use this in the app's entrypoint:

import {NavigationContainer} from '@react-navigation/native';

const App = () => {
  useInitPlayer();
  
  return (
    <NavigationContainer>
      <RootNavigator />
    </NavigationContainer>
  );
};
App.tsx

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>();

  // ...
player.utils.ts - usePlayerControls

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);
      }
    },
  );
  
  // ...
player.utils.ts - usePlayerControls

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,
  };
};
player.utils.ts - usePlayerControls

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},
  },
}) => {

  // ...
PlayerModal.tsx

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]);
  
  // ...
PlayerModal.tsx

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>
  );
};
PlayerModal.tsx

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>
  );
};
Controls.tsx

Here's the result:

PlayerModal

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:

HomeScreen

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>
  );
};
FloatinPlayer.tsx

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:

HomeScreen with the current track in FloatingPlayer

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