Farhan Kathawala I’m a full-stack developer with three years of experience in frontend development (I worked with Angular and Vue back then, and now I mostly work with React/React Native) and three years of experience in backend development, writing and maintaining APIs and microservices. I also have experience automating service provision and application setup on AWS/Azure/GCP.

Using Material UI in React Native

8 min read 2481

If you’re building a cross-platform mobile app, it’s a good idea to base your app’s UI/UX on Material Design, Google’s own design language, which it uses in all its mobile apps. Why? One of the best reasons is that many of the most popular mobile apps heavily use Material Design concepts: Whatsapp, Uber, Lyft, Google Maps, SpotAngels, etc. This means your users are already familiar with the look and feel of Material Design, and they will quickly and easily understand how to use your app if you adhere to the design language of their favorite, most commonly used apps.

The heavy hitter of Material Design component libraries on React Native is react-native-paper, and this guide will focus on using react-native-paper to set up a starter app with the some of the most prominent and recognizable Material Design features: Hamburger Menu, Drawer Navigation, FAB (Floating Action Button), and Contextual Action Bar.

Demo

This is what the starter app I’m going to build will eventually look like. As you read through the guide, you can reference the full code of this demo, which resides in the following GitHub repo: material-ui-in-react-native.

Gif of Material UI in React Native app demo

Setup

First, I’ll initialize my React Native app using Expo. You don’t have to use Expo, it just helps me get started so I can focus on the UI in this example.

If you don’t have expo-cli installed, then first run:

npm install -g expo-cli 
<

Now run the following:

expo init material-ui-in-react-native -t expo-template-blank-typescript
cd material-ui-in-react-native
yarn add react-native-paper 

You can also follow these additional installation instructions to enable tree shaking, reduce bundle size, etc. with react-native-paper.

I’m also adding react-navigation to this project. I recommend you use it as well. It’s the most popular navigation library for React Native, and there’s more support for running it alongside react-native-paper compared to other navigation libraries. Follow the installation instructions for react-navigation since they are slightly different depending on whether you use Expo or plain React Native.

Initial Screens

Create the following two files in your app’s main directory (if you want the styles used, remember everything for this example is available in this GitHub repo):

MyFriends.tsx

We made a custom demo for .
No really. Click here to check it out.

import React from 'react';
import {View} from 'react-native';
import {Title} from 'react-native-paper';
import base from './styles/base';

interface IMyFriendsProps {}

const MyFriends: React.FunctionComponent<IMyFriendsProps> = (props) => {
  return (
    <View style={base.centered}>
      <Title>MyFriends</Title>
    </View>
  );
};
export default MyFriends;

Profile.tsx

import React from 'react';
import {View} from 'react-native';
import {Title} from 'react-native-paper';
import base from './styles/base';

interface IProfileProps {}

const Profile: React.FunctionComponent<IProfileProps> = (props) => {
  return (
    <View style={base.centered}>
      <Title>Profile</Title>
    </View>
  );
};
export default Profile;

Over the course of this guide, I’ll link these screens to each other using a Navigation Drawer (or Hamburger Menu) and add Material UI components to each of them.

Hamburger Menu / Drawer Navigation

Material Design promotes the usage of a Navigation Drawer, so I’ll use this type of UI to make the My Friends and Profile screens navigable to and from each other.

First, I’ll add React Navigation’s drawer library:

yarn add @react-navigation/native @react-navigation/drawer

Now I’ll add the following into my App.tsx to enable the Drawer Navigation. It should look like the following:

App.tsx

import React from 'react';
import {createDrawerNavigator} from '@react-navigation/drawer';
import {NavigationContainer} from '@react-navigation/native';
import {StatusBar} from 'expo-status-bar';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import MyFriends from './MyFriends';
import Profile from './Profile';

export default function App() {
  const Drawer = createDrawerNavigator();
  return (
    <SafeAreaProvider>
        <NavigationContainer>
          <Drawer.Navigator>
            <Drawer.Screen name='My Friends' component={MyFriends} />
            <Drawer.Screen name='Profile' component={Profile} />
          </Drawer.Navigator>
        </NavigationContainer>
      <StatusBar style='auto' />
    </SafeAreaProvider>
  );
}

This drawer also needs a button to open it. That button should look like the classic hamburger icon (≡) and it should open the navigation drawer when pressed. Here’s what that button might look like:

components/MenuIcon.tsx

import React from 'react';
import {IconButton} from 'react-native-paper';
import {DrawerActions, useNavigation} from '@react-navigation/native';
import {useCallback} from 'react';

export default function MenuIcon() {
  const navigation = useNavigation();
  const openDrawer = useCallback(() => {
    navigation.dispatch(DrawerActions.openDrawer());
  }, []);

  return <IconButton icon='menu' size={24} onPress={openDrawer} />;
}

A few things to notice here:

React-navigation’s useNavigation hook is how we are going to execute most navigation actions, from changing screens to opening drawers.

The <IconButton> component is from react-native-paper. It supports all the Material Design icons by name and optionally supports any React Node that you want to pass in there, which allows one to add in any desired icon from any third party library.

Now I’ll add my <MenuIcon> to my Navigation Drawer by replacing this from App.tsx:

  <Drawer.Navigator>
    ...
  </Drawer.Navigator>

With the following:

import MenuIcon from './components/MenuIcon.tsx';
...
  <Drawer.Navigator
    screenOptions={{headerShown: true, headerLeft: () => <MenuIcon />}}
  >
    ...
  </Drawer.Navigator>

Lastly, I can customize my Navigation Drawer using the drawerContent prop of the same <Drawer.Navigator> component I just altered.

I’ll show an example which adds a header image to the top of the drawer. Feel free to customize with whatever you want to put in the drawer:

components/MenuContent.tsx

import React from 'react';
import {
  DrawerContentComponentProps,
  DrawerContentScrollView,
  DrawerItemList,
} from '@react-navigation/drawer';
import {Image} from 'react-native';
const MenuContent: React.FunctionComponent<DrawerContentComponentProps> = (
  props
) => {
  return (
    <DrawerContentScrollView {...props}>
      <Image
        resizeMode='cover'
        style={{width: '100%', height: 140}}
        source={require('../assets/drawerHeaderImage.jpg')}
      />
      <DrawerItemList {...props} />
    </DrawerContentScrollView>
  );
};
export default MenuContent;

Now I’ll pass <MenuContent> into <Drawer.Navigator>. To do this, I’ll make the following change in App.tsx from this:

import MenuIcon from './components/MenuIcon.tsx';
...
  <Drawer.Navigator
    screenOptions={{headerShown: true, headerLeft: () => <MenuIcon />}}
  >
    ...
  </Drawer.Navigator>

to this:

import MenuIcon from './components/MenuIcon.tsx';
import MenuContent from './components/MenuContent.tsx';
...
  <Drawer.Navigator
    screenOptions={{headerShown: true, headerLeft: () => <MenuIcon />}}
    drawerContent={(props) => <MenuContent {...props} />}
  >
    ...
  </Drawer.Navigator>

And now, I have fully functioning Drawer Navigation with a custom image header. Here’s the result:

Gif of Drawer Navigation

Next, I’ll flesh out the main screens with more Material Design concepts.

Floating Action Button (FAB)

One of the hallmarks of Material Design is the Floating Action Button (or FAB). The <FAB> and <FAB.Group> components provide a useful implementation of the Floating Action Button according to Material Design principles. With minimal setup, I’ll add this to the My Friends screen right now.

First, I’ll need to add the <Provider> component from react-native-paper and wrap that component around the <NavigationContainer> in App.tsx as follows:

App.tsx

import {Provider} from 'react-native-paper';
...
  <Provider>
    <NavigationContainer>
      ...
    </NavigationContainer>
  </Provider>

Now I’ll add my Floating Action Button to the My Friends screen. To do so, I’ll need the following:

  • The <Portal> and <FAB.Group> components from react-native-paper
  • A state variable fabIsOpen to keep track of whether the FAB is open or closed
  • Some information about whether or not this screen is currently visible to the user (isScreenFocused). I need isScreenFocused because without it, I might end up with the FAB being visible on other screens than the My Friends screen

Here’s what the My Friends screen looks like with all that added in:

MyFriends.tsx

import {useIsFocused} from '@react-navigation/native';
import React, {useState} from 'react';
import {View} from 'react-native';
import {FAB, Portal, Title} from 'react-native-paper';
import base from './styles/base';

interface IMyFriendsProps {}

const MyFriends: React.FunctionComponent<IMyFriendsProps> = (props) => {
  const isScreenFocused = useIsFocused();
  const [fabIsOpen, setFabIsOpen] = useState(false);

  return (
    <View style={base.centered}>
      <Title>MyFriends</Title>
      <Portal>
        <FAB.Group
          visible={isScreenFocused}
          open={fabIsOpen}
          onStateChange={({open}) => setFabIsOpen(open)}
          icon={fabIsOpen ? 'close' : 'account-multiple'}
          actions={[
            {
              icon: 'plus',
              label: 'Add new friend',
              onPress: () => {},
            },
            {
              icon: 'file-export',
              label: 'Export friend list',
              onPress: () => {},
            },
          ]}
        />
      </Portal>
    </View>
  );
};
export default MyFriends;

Now the My Friends screen behaves like the following:

Gif of floating action button demonstration

Next, I’ll add a Contextual Action Bar, which can be activated whenever an item in one of the screens is long pressed.

Contextual Action Bar

Apps like Gmail and Google Photos make use of a Material Design concept called the Contextual Action Bar. I’ll implement a version of this quickly in the current app.

First, I’ll build the ContextualActionBar component itself using the Appbar component from react-native-paper. It should look something like this, to start with:

./components/ContextualActionBar.tsx

import React from 'react';
import {Appbar} from 'react-native-paper';

interface IContextualActionBarProps {}

const ContextualActionBar: React.FunctionComponent<IContextualActionBarProps> = (
  props
) => {
  return (
    <Appbar.Header {...props} style={{width: '100%'}}>
      <Appbar.Action icon='close' onPress={() => {}} />
      <Appbar.Content title='' />
      <Appbar.Action icon='delete' onPress={() => {}} />
      <Appbar.Action icon='content-copy' onPress={() => {}} />
      <Appbar.Action icon='magnify' onPress={() => {}} />
      <Appbar.Action icon='dots-vertical' onPress={() => {}} />
    </Appbar.Header>
  );
};
export default ContextualActionBar;

Now I want this component to render on top of the given screen’s header whenever an item is long pressed. Back in the My Friends screen, I’ve added some items for this purpose. On that screen, here’s how I’ll render the Contextual Action Bar over the screen’s header:

MyFriends.tsx

import {useNavigation} from '@react-navigation/native';
import ContextualActionBar from './components/ContextualActionBar';
...
  const [cabIsOpen, setCabIsOpen] = useState(false);
  const navigation = useNavigation();

  const openHeader = useCallback(() => {
    setCabIsOpen(!cabIsOpen);
  }, [cabIsOpen]);

  useEffect(() => {
    if (cabIsOpen) {
      navigation.setOptions({
        // have to use props: any since that's the type signature
        // from react-navigation...
        header: (props: any) => (<ContextualActionBar {...props} />),
      });
    } else {
      navigation.setOptions({header: undefined});
    }
  }, [cabIsOpen]);
...
  return (
    ...
    <List.Item
      title='Friend #1'
      description='Mar 18 | 3:31 PM'
      style={{width: '100%'}}
      onPress={() => {}}
      onLongPress={openHeader}
    />
    ...
  );

Above, I’m toggling a state boolean value (cabIsOpen) whenever a given item is long pressed. Based on that value, I either switch the React Navigation header to render the <ContextualActionBar> or switch back to render the default React Navigation header.

Now I should have a Contextual Action Bar appear when I long press the “Friend #1” item. However, the title is still empty and I cannot do anything in any of the actions because the <ContextualActionBar> is unaware of any of the state of either the “Friend #1” item or the larger My Friends screen as a whole.

Thus, the next step is to add a title into the <ContextualActionBar> and pass in a function that can close the bar and be triggered by one of the buttons in the bar.

To do this, I have to add another state variable to the My Friends screen:

  const [selectedItemName, setSelectedItemName] = useState('');

I also need to create a function which will close the header and reset the above state variable:

  const closeHeader = useCallback(() => {
    setCabIsOpen(false);
    setSelectedItemName('');
  }, []);

Then I need to pass both selectedItemName and closeHeader as props to <ContextualActionBar>:

  useEffect(() => {
    if (cabIsOpen) {
      navigation.setOptions({
        header: (props: any) => (
          <ContextualActionBar
            {...props}
            title={selectedItemName}
            close={closeHeader}
          />
      ),
      });
    } else {
      navigation.setOptions({header: undefined});
    }
  }, [cabIsOpen, selectedItemName]);

Lastly, I need to set selectedItemName to the title of the item that’s been long pressed:

  ...
  const openHeader = useCallback((str: string) => {
    setSelectedItemName(str);
    setCabIsOpen(!cabIsOpen);
  }, [cabIsOpen]);
  ...
  return (
    ...
    <List.Item
      title='Friend #1'
      ...
      onLongPress={() => openHeader('Friend #1')}
    />
  );

And now I can use the title and close props in <ContextualActionBar> as follows:

./components/ContextualActionBar.tsx

interface IContextualActionBarProps {
  title: string;
  close: () => void;
}
...
  return (
      ...
      <Appbar.Action icon='close' onPress={props.close} />
      <Appbar.Content title={props.title} />
      ...
  );

Now, I have a functional, Material Design-inspired Contextual Action Bar, utilizing react-native-paper and react-navigation, which looks like the following:

Contextual Action Bar, activates when the user long presses an item
Contextual Action Bar, activates when the user long presses an item

Theming

The last thing I want to do is theme my app so I can change the primary color, secondary color, text colors, etc.

Theming is a little tricky because both react-navigation and react-native-paper have their own ThemeProvider components, and they can easily conflict with each other. Fortunately, there’s a great guide available on how to theme an app which uses both react-native-paper and react-navigation. If you follow this, you should be all set to go.

I’ll add in a little extra help for those who use Typescript and would run into esoteric errors trying to follow the above guide.

First, I’ll create a theme file which looks like the following. A few things to note are:

  • The return type of combineThemes encompasses both ReactNavigationTheme and ReactNativePaper.Theme
  • I changed the primary and accent colors, which will affect the CAB and FAB respectively
  • I added a new color to the theme called animationColor. If you don’t want to add a new color, you don’t need to declare the global namespace

theme.ts

import {
  DarkTheme as NavigationDarkTheme,
  DefaultTheme as NavigationDefaultTheme,
  Theme,
} from '@react-navigation/native';
import {ColorSchemeName} from 'react-native';
import {
  DarkTheme as PaperDarkTheme,
  DefaultTheme as PaperDefaultTheme,
} from 'react-native-paper';
declare global {
  namespace ReactNativePaper {
    interface ThemeColors {
      animationColor: string;
    }
    interface Theme {
      statusBar: 'light' | 'dark' | 'auto' | 'inverted' | undefined;
    }
  }
}
interface ReactNavigationTheme extends Theme {
  statusBar: 'light' | 'dark' | 'auto' | 'inverted' | undefined;
}
export function combineThemes(
  themeType: ColorSchemeName
): ReactNativePaper.Theme | ReactNavigationTheme {
  const CombinedDefaultTheme: ReactNativePaper.Theme = {
    ...NavigationDefaultTheme,
    ...PaperDefaultTheme,
    statusBar: 'dark',
    colors: {
      ...NavigationDefaultTheme.colors,
      ...PaperDefaultTheme.colors,
      animationColor: '#2922ff',
      primary: '#079c20',
      accent: '#2922ff',
    },
  };
  const CombinedDarkTheme: ReactNativePaper.Theme = {
    ...NavigationDarkTheme,
    ...PaperDarkTheme,
    mode: 'adaptive',
    statusBar: 'light',
    colors: {
      ...NavigationDarkTheme.colors,
      ...PaperDarkTheme.colors,
      animationColor: '#6262ff',
      primary: '#079c20',
      accent: '#2922ff',
    },
  };
  return themeType === 'dark' ? CombinedDarkTheme : CombinedDefaultTheme;
}

Then, back in App.tsx I’ll add my theme to both the react-native-paper Provider component and the NavigationContainer component from react-navigation as follows:

App.tsx

import {useColorScheme} from 'react-native';
import {NavigationContainer, Theme} from '@react-navigation/native';
import {combineThemes} from './theme';
...
  const colorScheme = useColorScheme() as 'light' | 'dark';
  const theme = combineThemes(colorScheme);
  ...
    <Provider theme={theme as ReactNativePaper.Theme}>
      <NavigationContainer theme={theme as Theme}>
      </NavigationContainer>
    </Provider>

I am using Expo, so I additionally need to add the following in app.json to enable dark mode. You may not need to, however.

    "userInterfaceStyle": "automatic",

And now, a custom themed, dark mode enabled, Material Design-inspired app! It looks great!

Contextual Action Bar and Floating Action Button with custom colors, light theme Drawer open, showing header image, light mode Contextual Action Bar and Floating Action Button with custom colors, dark themeDrawer open, showing header image, dark mode

Conclusion

If you followed along to the end with me here, then you should have your own cross-platform app with Material Design elements from the react-native-paper library like Drawer Navigation (with custom designs in the drawer menu), Floating Action Buttons, and Contextual Action Bars. You should also have theming enabled which plays nicely with both the react-native-paper and react-navigation libraries. This setup should enable you to quickly and stylishly build out your next mobile app with ease.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Farhan Kathawala I’m a full-stack developer with three years of experience in frontend development (I worked with Angular and Vue back then, and now I mostly work with React/React Native) and three years of experience in backend development, writing and maintaining APIs and microservices. I also have experience automating service provision and application setup on AWS/Azure/GCP.

Leave a Reply